summaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/starship-cache/Cargo.toml18
-rw-r--r--crates/starship-cache/src/errors.rs17
-rw-r--r--crates/starship-cache/src/lib.rs312
-rw-r--r--crates/starship_module_config_derive/Cargo.toml25
-rw-r--r--crates/starship_module_config_derive/LICENSE15
-rw-r--r--crates/starship_module_config_derive/README.md1
-rw-r--r--crates/starship_module_config_derive/src/lib.rs90
7 files changed, 478 insertions, 0 deletions
diff --git a/crates/starship-cache/Cargo.toml b/crates/starship-cache/Cargo.toml
new file mode 100644
index 000000000..bb66cd150
--- /dev/null
+++ b/crates/starship-cache/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "starship-cache"
+version = "0.1.0"
+authors = ["Starship Contributors"]
+description = "Intelligent caching for Starship"
+edition = "2018"
+license = "ISC"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+dirs = "3.0.2"
+serde = { version = "1.0.126", features = ["derive"] }
+thiserror = "1.0.25"
+toml = "0.5.8"
+
+[dev-dependencies]
+tempfile = "3.2.0"
diff --git a/crates/starship-cache/src/errors.rs b/crates/starship-cache/src/errors.rs
new file mode 100644
index 000000000..248a3af3b
--- /dev/null
+++ b/crates/starship-cache/src/errors.rs
@@ -0,0 +1,17 @@
+use std::io;
+
+#[non_exhaustive]
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("cannot open cache file")]
+ OpenFile(#[source] io::Error),
+
+ #[error("cannot write cache file")]
+ WriteFile(#[source] io::Error),
+
+ #[error("cannot read binary metadata")]
+ ReadMetadata(#[source] io::Error),
+
+ #[error("unable to serialize cache")]
+ SerializeCache(#[source] toml::ser::Error),
+}
diff --git a/crates/starship-cache/src/lib.rs b/crates/starship-cache/src/lib.rs
new file mode 100644
index 000000000..77fa0bb76
--- /dev/null
+++ b/crates/starship-cache/src/lib.rs
@@ -0,0 +1,312 @@
+//! The on-disk caching functionality for Starship.
+//!
+//! This module contains the caching mechanism allowing Starship to reuse the
+//! output of previously run commands when possible.
+//!
+//! The cache stores the output of commands, and the metadata of the binaries
+//! being called at the time the command is run. When the binary's metadata
+//! changes, the cache clears all the values of the commands calling that binary.
+//!
+//! The goals of this library are to be quick to cache outputs, quick to retreive
+//! cached values, compatible with version-managed tools, and easy to troubleshoot.
+
+pub mod errors;
+
+pub use errors::Error;
+use serde::{Deserialize, Serialize};
+use std::{
+ collections::HashMap,
+ convert::TryFrom,
+ fs::{self, OpenOptions},
+ io::Read,
+ path::{Path, PathBuf},
+ process::Output,
+ time::UNIX_EPOCH,
+};
+
+type FullCommand = String;
+type BinaryPath = PathBuf;
+
+const CURRENT_VERSION: u8 = 1;
+
+/// An instance of the binary output cache
+pub struct Cache {
+ /// The path of the cache file the cache serializes to
+ path: PathBuf,
+ /// Whether the cache has been changed and requires writing to disk
+ changed: bool,
+ /// The cache's internal state
+ contents: CacheContents,
+}
+
+impl Cache {
+ /// Create or parse a cache file at the given path
+ pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
+ let mut file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .create(true)
+ .open(&path)
+ .map_err(Error::OpenFile)?;
+ let mut contents = String::new();
+
+ // Clear the cache if it is not valid UTF-8
+ file.read_to_string(&mut contents).unwrap_or_default();
+
+ // Clear the cache if it unable to be parsed
+ let mut cache: CacheContents = toml::from_str(&contents).unwrap_or_default();
+
+ // Clear the cache if it is not using the current version
+ if cache.version != CURRENT_VERSION {
+ cache = CacheContents::default();
+ }
+
+ Ok(Self {
+ path: path.as_ref().to_owned(),
+ changed: false,
+ contents: cache,
+ })
+ }
+
+ /// Get the output of the given command if it has been previously cached
+ pub fn get(&mut self, binary_path: &Path, command: &str) -> Option<&CachedOutput> {
+ let bin = self.contents.binaries.get(binary_path)?;
+
+ let current_metadata = BinaryMetadata::try_from(binary_path).ok()?;
+ let is_stale = current_metadata != bin.metadata;
+ if is_stale {
+ return None;
+ };
+
+ bin.commands.get(command)
+ }
+
+ /// Set the cached output of the given command
+ pub fn set<O: Into<CachedOutput>>(&mut self, binary_path: &Path, command: &str, output: O) {
+ let current_metadata = match BinaryMetadata::try_from(binary_path) {
+ Ok(metadata) => metadata,
+ // Skip caching if unable to read binary metadata
+ Err(_e) => return,
+ };
+ let mut bin = self
+ .contents
+ .binaries
+ .entry(binary_path.to_path_buf())
+ .or_insert(BinaryCache {
+ metadata: current_metadata.clone(),
+ commands: HashMap::new(),
+ });
+
+ let is_stale = current_metadata != bin.metadata;
+ if is_stale {
+ bin.metadata = current_metadata;
+ bin.commands.clear();
+ };
+
+ bin.commands.insert(command.to_owned(), output.into());
+ self.changed = true;
+ }
+
+ /// Write any cache updates to disk
+ pub fn write(&self) -> Result<(), Error> {
+ if !self.changed {
+ return Ok(());
+ };
+
+ let contents = toml::to_string(&self.contents).map_err(Error::SerializeCache)?;
+ fs::write(&self.path, contents).map_err(Error::WriteFile)?;
+ Ok(())
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct CacheContents {
+ /// The version of the cache file
+ version: u8,
+ /// A mapping of binaries' paths and their caches
+ binaries: HashMap<BinaryPath, BinaryCache>,
+}
+
+impl Default for CacheContents {
+ fn default() -> Self {
+ Self {
+ version: CURRENT_VERSION,
+ binaries: HashMap::new(),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct BinaryCache {
+ /// The metadata of the binary at the time it was last called
+ /// If the binary's metadata changes, its cached data is cleared
+ metadata: BinaryMetadata,
+ /// A mapping of commands and their cached outputs
+ commands: HashMap<FullCommand, CachedOutput>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct CachedOutput {
+ pub stdout: Vec<u8>,
+ pub stderr: Vec<u8>,
+ pub status: Option<i32>,
+}
+
+impl CachedOutput {
+ pub fn success(&self) -> bool {
+ self.status == Some(0)
+ }
+}
+
+impl From<Output> for CachedOutput {
+ fn from(output: Output) -> Self {
+ Self {
+ stdout: output.stdout,
+ stderr: output.stderr,
+ status: output.status.code()
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+struct BinaryMetadata {
+ size: u64,
+ is_dir: bool,
+ is_file: bool,
+ readonly: bool,
+ c_time: u64,
+ m_time: u64,
+}
+
+impl TryFrom<&Path> for BinaryMetadata {
+ type Error = crate::Error;
+
+ fn try_from(path: &Path) -> Result<Self, Error> {
+ let metadata = fs::metadata(path).map_err(Error::ReadMetadata)?;
+
+ // If ctime or mtime are not provided, store `0` in their place
+ let c_time = match metadata.created() {
+ Err(_e) => 0,
+ Ok(t) => t
+ .duration_since(UNIX_EPOCH)
+ .map(|t| t.as_secs())
+ .unwrap_or(0),
+ };
+
+ let m_time = match metadata.modified() {
+ Err(_e) => 0,
+ Ok(t) => t
+ .duration_since(UNIX_EPOCH)
+ .map(|t| t.as_secs())
+ .unwrap_or(0),
+ };
+
+ Ok(Self {
+ size: metadata.len(),
+ is_dir: metadata.is_dir(),
+ is_file: metadata.is_file(),
+ readonly: metadata.permissions().readonly(),
+ c_time,
+ m_time,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::{fs::File, io::Write};
+ use tempfile::tempdir;
+
+ type Result = std::result::Result<(), Box<dyn std::error::Error>>;
+
+ #[test]
+ fn empty_cache_file_is_created() -> Result {
+ let dir = tempdir()?;
+ let cache_path = Path::join(dir.path(), "bin-cache");
+ let cache = Cache::new(&cache_path)?;
+ cache.write()?;
+
+ assert!(Path::exists(&cache_path));
+ Ok(())
+ }
+
+ #[test]
+ fn retreive_from_populated_cache() -> Result {
+ let dir = tempdir()?;
+ let cache_path = dir.path().join("bin-cache");
+ let mut cache = Cache::new(&cache_path)?;
+
+ // Create "node" binary
+ let bin_path = dir.path().join("node");
+ File::create(&bin_path)?;
+
+ // Populate cache with "node" output
+ let expected = "v14.16.0";
+ cache.set(&bin_path, "node --version", &expected);
+ cache.write()?;
+
+ // Retreive cached output
+ let mut new_cache = Cache::new(&cache_path)?;
+ let actual = new_cache.get(&bin_path, "node --version").unwrap();
+
+ assert_eq!(expected, actual);
+ Ok(())
+ }
+
+ #[test]
+ fn overrites_stale_cache() -> Result {
+ let dir = tempdir()?;
+ let cache_path = dir.path().join("bin-cache");
+ let mut cache = Cache::new(&cache_path)?;
+
+ // Create "node" binary
+ let bin_path = dir.path().join("node");
+ File::create(&bin_path)?;
+
+ // Populate cache with "node" output
+ let expected = "v14.16.0";
+ cache.set(&bin_path, "node -v", &expected);
+ cache.set(&bin_path, "node --help", &expected);
+ cache.set(&bin_path, "node --version", &expected);
+ cache.write()?;
+
+ // Update "node" binary
+ File::create(&bin_path)?.write(b"updated")?;
+
+ // Retreive cached output
+ let mut new_cache = Cache::new(&cache_path)?;
+
+ // Set a cached value again
+ new_cache.set(&bin_path, "node -v", "v15.0.0");
+
+ // The other, previously cached values, should be cleared as stale
+ assert_eq!(new_cache.get(&bin_path, "node --version"), None);
+ assert_eq!(new_cache.get(&bin_path, "node --help"), None);
+ Ok(())
+ }
+
+ #[test]
+ fn doesnt_retreive_stale_cache() -> Result {
+ let dir = tempdir()?;
+ let cache_path = dir.path().join("bin-cache");
+ let mut cache = Cache::new(&cache_path)?;
+
+ // Create "node" binary
+ let bin_path = dir.path().join("node");
+ File::create(&bin_path)?;
+
+ // Populate cache with "node" output
+ cache.set(&bin_path, "node --version", "v14.16.0");
+ cache.write()?;
+
+ // Update "node" binary
+ File::create(&bin_path)?.write(b"updated")?;
+
+ let mut new_cache = Cache::new(&cache_path)?;
+ let actual = new_cache.get(&bin_path, "node --version");
+
+ assert_eq!(None, actual);
+ Ok(())
+ }
+}
diff --git a/crates/starship_module_config_derive/Cargo.toml b/crates/starship_module_config_derive/Cargo.toml
new file mode 100644
index 000000000..928eef5f6
--- /dev/null
+++ b/crates/starship_module_config_derive/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "starship_module_config_derive"
+version = "0.2.1"
+edition = "2018"
+authors = ["Matan Kushner <hello@matchai.me>"]
+homepage = "https://starship.rs"
+documentation = "https://starship.rs/guide/"
+repository = "https://github.com/starship/starship"
+readme = "README.md"
+license = "ISC"
+keywords = ["prompt", "shell", "bash", "fish", "zsh"]
+categories = ["command-line-utilities"]
+description = """
+The minimal, blazing-fast, and infinitely customizable prompt for any shell! ☄🌌️
+"""
+include = ["src/**/*", "LICENSE", "README.md"]
+
+[lib]
+name = "starship_module_config_derive"
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "~1"
+quote = "~1"
+syn = "~1"
diff --git a/crates/starship_module_config_derive/LICENSE b/crates/starship_module_config_derive/LICENSE
new file mode 100644
index 000000000..1ce6f8232
--- /dev/null
+++ b/crates/starship_module_config_derive/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2019, Starship Contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/crates/starship_module_config_derive/README.md b/crates/starship_module_config_derive/README.md
new file mode 100644
index 000000000..b1a0ca9ee
--- /dev/null
+++ b/crates/starship_module_config_derive/README.md
@@ -0,0 +1 @@
+# starship_module_config_derive
diff --git a/crates/starship_module_config_derive/src/lib.rs b/crates/starship_module_config_derive/src/lib.rs
new file mode 100644
index 000000000..40d4301b9
--- /dev/null
+++ b/crates/starship_module_config_derive/src/lib.rs
@@ -0,0 +1,90 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput};
+
+#[proc_macro_derive(ModuleConfig)]
+pub fn derive_module_config(input: TokenStream) -> TokenStream {
+ let dinput = parse_macro_input!(input as DeriveInput);
+ impl_module_config(dinput)
+}
+
+fn impl_module_config(dinput: DeriveInput) -> proc_macro::TokenStream {
+ let struct_ident = &dinput.ident;
+ let (_impl_generics, ty_generics, where_clause) = dinput.generics.split_for_impl();
+
+ let mut from_config = quote! {};
+ let mut load_config = quote! {};
+
+ if let syn::Data::Struct(data) = dinput.data {
+ if let syn::Fields::Named(fields_named) = data.fields {
+ let mut load_tokens = quote! {};
+ let mut fields = quote! {};
+
+ for field in fields_named.named.iter() {
+ let ident = field.ident.as_ref().unwrap();
+
+ let new_load_tokens = quote! {
+ stringify!(#ident) => self.#ident.load_config(v),
+ };
+
+ let new_field = quote! {
+ stringify!(#ident),
+ };
+
+ load_tokens = quote! {
+ #load_tokens
+ #new_load_tokens
+ };
+
+ fields = quote! {
+ #fields
+ #new_field
+ };
+ }
+
+ load_config = quote! {
+ fn load_config(&mut self, config: &'a toml::Value) {
+ if let toml::Value::Table(config) = config {
+ config.iter().for_each(|(k, v)| {
+ match k.as_str() {
+ #load_tokens
+ unknown => {
+ ::log::warn!("Unknown config key '{}'", unknown);
+
+ let did_you_mean = ::std::array::IntoIter::new([#fields])
+ .filter_map(|field| {
+ let score = ::strsim::jaro_winkler(unknown, field);
+ (score > 0.8).then(|| (score, field))
+ })
+ .max_by(
+ |(score_a, _field_a), (score_b, _field_b)| {
+ score_a.partial_cmp(score_b).unwrap_or(::std::cmp::Ordering::Equal)
+ },
+ );
+
+ if let Some((_score, field)) = did_you_mean {
+ ::log::warn!("Did you mean '{}'?", field);
+ }
+ },
+ }
+ });
+ }
+ }
+ };
+ from_config = quote! {
+ fn from_config(config: &'a toml::Value) -> Option<Self> {
+ let mut out = Self::default();
+ out.load_config(config);
+ Some(out)
+ }
+ };
+ }
+ }
+
+ TokenStream::from(quote! {
+ impl<'a> ModuleConfig<'a> for #struct_ident #ty_generics #where_clause {
+ #from_config
+ #load_config
+ }
+ })
+}