diff --git a/Cargo.lock b/Cargo.lock index 5a4b32dc0..85233979a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1978,7 +1978,6 @@ dependencies = [ "openssl", "path-absolutize", "petgraph", - "platform-info", "predicates", "pretty_assertions", "rand", @@ -2548,16 +2547,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "platform-info" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ff316b9c4642feda973c18f0decd6c8b0919d4722566f6e4337cce0dd88217" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "portable-atomic" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index bbbfd5d38..9e6dc0eae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,12 +74,11 @@ indicatif = { version = "0.17.8", features = ["default", "improved_unicode"] } indoc = "2.0.5" itertools = "0.13" log = "0.4.21" -num_cpus = "1.16.0" # gets cross-platform the number of CPU +num_cpus = "1" once_cell = "1.19.0" openssl = { version = "0.10.66", optional = true } path-absolutize = "3.1.1" petgraph = "0.6.4" -platform-info = "2.0.3" # cross-platform platform information rand = "0.8.5" rayon = "1.10.0" regex = "1.10.4" diff --git a/docs/templates.md b/docs/templates.md index 167e653e2..bdc7604c0 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -9,11 +9,49 @@ Templates are used in the following locations: The following context objects are available inside templates: - `env: HashMap` – current environment variables -- `config_root: PathBuf` – directory containing the `.mise.toml` file +- `cwd: PathBuf` – current working directory +- `config_root: PathBuf` – directory containing the `mise.toml` file or directory containing + `.mise` directory with config file. As well as these functions: -- `exec(command: &str) -> String` – execute a command and return the output +- `exec(command) -> String` – execute a command and return the output +- `arch() -> String` – return the system architecture, e.g. `x86_64`, `arm64` +- `os() -> String` – return the operating system, e.g. `linux`, `macos`, `windows` +- `os_family() -> String` – return the operating system family, e.g. `unix`, `windows` +- `num_cpus() -> usize` – return the number of CPUs on the system + +And these filters: + +- `str | hash -> String` – return the SHA256 hash of the input string +- `str | hash(len=usize) -> String` – return the SHA256 hash of the input string truncated to `len` + characters +- `path | hash_file -> String` – return the SHA256 hash of the file at the input path +- `path | hash_file(len=usize) -> String` – return the SHA256 hash of the file at the input path + truncated to `len` characters +- `path | canonicalize -> String` – return the canonicalized path +- `path | dirname -> String` – return the directory path for a file, e.g. `/foo/bar/baz.txt` -> + `/foo/bar` +- `path | basename -> String` – return the base name of a file, e.g. `/foo/bar/baz.txt` -> `baz.txt` +- `path | extname -> String` – return the extension of a file, e.g. `/foo/bar/baz.txt` -> `.txt` +- `path | file_stem -> String` – return the file name without the extension, e.g. + `/foo/bar/baz.txt` -> `baz` +- `path | file_size -> String` – return the size of a file in bytes +- `path | last_modified -> String` – return the last modified time of a file +- `path[] | join_path -> String` – join an array of paths into a single path +- `str | quote -> String` – quote a string +- `str | kebabcase -> String` – convert a string to kebab-case +- `str | lowercamelcase -> String` – convert a string to lowerCamelCase +- `str | uppercamelcase -> String` – convert a string to UpperCamelCase +- `str | shoutycamelcase -> String` – convert a string to ShoutyCamelCase +- `str | snakecase -> String` – convert a string to snake_case +- `str | shoutysnakecase -> String` – convert a string to SHOUTY_SNAKE_CASE + +And these testers: + +- `if path is dir` – if the path is a directory +- `if path is file` – if the path is a file +- `if path is exists` – if the path exists Templates are parsed with [tera](https://keats.github.io/tera/docs/)—which is quite powerful. For example, this snippet will get the directory name of the project: diff --git a/src/config/env_directive.rs b/src/config/env_directive.rs index b4f2387a3..184b8c449 100644 --- a/src/config/env_directive.rs +++ b/src/config/env_directive.rs @@ -97,6 +97,7 @@ impl EnvResults { .map(Path::to_path_buf) .or_else(|| dirs::CWD.clone()) .unwrap_or_default(); + ctx.insert("cwd", &*dirs::CWD); ctx.insert("config_root", &config_root); let env_vars = env .iter() diff --git a/src/file.rs b/src/file.rs index b4ce91bdc..2c4567840 100644 --- a/src/file.rs +++ b/src/file.rs @@ -22,6 +22,12 @@ use zip::ZipArchive; use crate::{dirs, env}; +pub fn open>(path: P) -> Result { + let path = path.as_ref(); + trace!("open {}", display_path(path)); + File::open(path).wrap_err_with(|| format!("failed open: {}", display_path(path))) +} + pub fn remove_all>(path: P) -> Result<()> { let path = path.as_ref(); match path.metadata().map(|m| m.file_type()) { diff --git a/src/hash.rs b/src/hash.rs index e85c3657f..176ced0e4 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -1,29 +1,34 @@ use std::collections::HashMap; -use std::fs::File; use std::hash::{Hash, Hasher}; use std::io::{Read, Write}; use std::path::Path; +use crate::file; +use crate::file::display_path; +use crate::ui::progress_report::SingleReport; use eyre::{ensure, Result}; use rayon::prelude::*; use sha2::{Digest, Sha256}; use siphasher::sip::SipHasher; -use crate::file::display_path; -use crate::ui::progress_report::SingleReport; - pub fn hash_to_str(t: &T) -> String { let mut s = SipHasher::new(); t.hash(&mut s); format!("{:x}", s.finish()) } +pub fn hash_sha256_to_str(s: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(s); + format!("{:x}", hasher.finalize()) +} + pub fn file_hash_sha256(path: &Path) -> Result { file_hash_sha256_prog(path, None) } pub fn file_hash_sha256_prog(path: &Path, pr: Option<&dyn SingleReport>) -> Result { - let mut file = File::open(path)?; + let mut file = file::open(path)?; if let Some(pr) = pr { pr.set_length(file.metadata()?.len()); } diff --git a/src/tera.rs b/src/tera.rs index 72972f5f4..aa86f16c5 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -6,12 +6,10 @@ use heck::{ ToUpperCamelCase, }; use once_cell::sync::Lazy; -use platform_info::{PlatformInfo, PlatformInfoAPI, UNameAPI}; use tera::{Context, Tera, Value}; use crate::cmd::cmd; -use crate::env; -use crate::hash::hash_to_str; +use crate::{env, hash}; pub static BASE_CONTEXT: Lazy = Lazy::new(|| { let mut context = Context::new(); @@ -44,9 +42,14 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { tera.register_function( "arch", move |_args: &HashMap| -> tera::Result { - let info = PlatformInfo::new().expect("unable to determine platform info"); - let result = String::from(info.machine().to_string_lossy()); // ignore potential UTF-8 convension error - Ok(Value::String(result)) + let arch = if cfg!(target_arch = "x86_64") { + "x64" + } else if cfg!(target_arch = "aarch64") { + "arm64" + } else { + env::consts::ARCH + }; + Ok(Value::String(arch.to_string())) }, ); tera.register_function( @@ -59,33 +62,39 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { tera.register_function( "os", move |_args: &HashMap| -> tera::Result { - let info = PlatformInfo::new().expect("unable to determine platform info"); - let result = String::from(info.osname().to_string_lossy()); // ignore potential UTF-8 convension error - Ok(Value::String(result)) + Ok(Value::String(env::consts::OS.to_string())) }, ); tera.register_function( "os_family", move |_args: &HashMap| -> tera::Result { - let info = PlatformInfo::new().expect("unable to determine platform info"); - let result = String::from(info.sysname().to_string_lossy()); // ignore potential UTF-8 convension error - Ok(Value::String(result)) + Ok(Value::String(env::consts::FAMILY.to_string())) }, ); - tera.register_function( - "invocation_directory", - move |_args: &HashMap| -> tera::Result { - let path = env::current_dir().unwrap_or_default(); - - let result = String::from(path.to_string_lossy()); - - Ok(Value::String(result)) + tera.register_filter( + "hash_file", + move |input: &Value, args: &HashMap| match input { + Value::String(s) => { + let path = Path::new(s); + let mut hash = hash::file_hash_sha256(path).unwrap(); + if let Some(len) = args.get("len").and_then(Value::as_u64) { + hash = hash.chars().take(len as usize).collect(); + } + Ok(Value::String(hash)) + } + _ => Err("hash input must be a string".into()), }, ); tera.register_filter( "hash", - move |input: &Value, _args: &HashMap| match input { - Value::String(s) => Ok(Value::String(hash_to_str(s))), + move |input: &Value, args: &HashMap| match input { + Value::String(s) => { + let mut hash = hash::hash_sha256_to_str(s); + if let Some(len) = args.get("len").and_then(Value::as_u64) { + hash = hash.chars().take(len as usize).collect(); + } + Ok(Value::String(hash)) + } _ => Err("hash input must be a string".into()), }, ); @@ -99,6 +108,58 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { _ => Err("hash input must be a string".into()), }, ); + tera.register_filter( + "dirname", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => { + let p = Path::new(s).parent().unwrap(); + Ok(Value::String(p.to_string_lossy().to_string())) + } + _ => Err("dirname input must be a string".into()), + }, + ); + tera.register_filter( + "basename", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => { + let p = Path::new(s).file_name().unwrap(); + Ok(Value::String(p.to_string_lossy().to_string())) + } + _ => Err("basename input must be a string".into()), + }, + ); + tera.register_filter( + "extname", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => { + let p = Path::new(s).extension().unwrap(); + Ok(Value::String(p.to_string_lossy().to_string())) + } + _ => Err("extname input must be a string".into()), + }, + ); + tera.register_filter( + "file_stem", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => { + let p = Path::new(s).file_stem().unwrap(); + Ok(Value::String(p.to_string_lossy().to_string())) + } + _ => Err("filename input must be a string".into()), + }, + ); + tera.register_filter( + "file_size", + move |input: &Value, _args: &HashMap| match input { + Value::String(s) => { + let p = Path::new(s); + let metadata = p.metadata()?; + let size = metadata.len(); + Ok(Value::Number(size.into())) + } + _ => Err("file_size input must be a string".into()), + }, + ); tera.register_filter( "last_modified", move |input: &Value, _args: &HashMap| match input { @@ -178,10 +239,24 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { }, ); tera.register_tester( - "file_exists", + "dir", + move |input: Option<&Value>, _args: &[Value]| match input { + Some(Value::String(s)) => Ok(Path::new(s).is_dir()), + _ => Err("is_dir input must be a string".into()), + }, + ); + tera.register_tester( + "file", + move |input: Option<&Value>, _args: &[Value]| match input { + Some(Value::String(s)) => Ok(Path::new(s).is_file()), + _ => Err("is_file input must be a string".into()), + }, + ); + tera.register_tester( + "exists", move |input: Option<&Value>, _args: &[Value]| match input { Some(Value::String(s)) => Ok(Path::new(s).exists()), - _ => Err("file_exists input must be a string".into()), + _ => Err("exists input must be a string".into()), }, ); @@ -191,33 +266,24 @@ pub fn get_tera(dir: Option<&Path>) -> Tera { #[cfg(test)] mod tests { use super::*; + use crate::test::reset; + use insta::assert_snapshot; #[test] - #[cfg(target_arch = "x86_64")] - fn test_render_with_custom_function_arch_x86_64() { - let mut tera = get_tera(Option::default()); - - let result = tera - .render_str("{{ arch() }}", &Context::default()) - .unwrap(); - - assert_eq!("x86_64", result); - } - - #[test] - #[cfg(target_arch = "aarch64")] - fn test_render_with_custom_function_arch_arm64() { - let mut tera = get_tera(Option::default()); - - let result = tera - .render_str("{{ arch() }}", &Context::default()) - .unwrap(); - - assert_eq!("aarch64", result); + fn test_render_with_custom_function_arch() { + reset(); + if cfg!(target_arch = "x86_64") { + assert_eq!(render("{{arch()}}"), "x64"); + } else if cfg!(target_arch = "aarch64") { + assert_eq!(render("{{arch()}}"), "arm64"); + } else { + assert_eq!(render("{{arch()}}"), env::consts::ARCH); + } } #[test] fn test_render_with_custom_function_num_cpus() { + reset(); let mut tera = get_tera(Option::default()); let result = tera @@ -229,80 +295,30 @@ mod tests { } #[test] - #[cfg(target_os = "linux")] - fn test_render_with_custom_function_os_linux() { - let mut tera = get_tera(Option::default()); - - let result = tera.render_str("{{ os() }}", &Context::default()).unwrap(); - - assert_eq!("GNU/Linux", result); - } - - #[test] - #[cfg(target_os = "windows")] - fn test_render_with_custom_function_os_windows() { - let mut tera = get_tera(Option::default()); - - let result = tera.render_str("{{ os() }}", &Context::default()).unwrap(); - - assert_eq!("Windows", result); - } - - #[test] - #[cfg(target_family = "unix")] - fn test_render_with_custom_function_os_family_unix() { - let mut tera = get_tera(Option::default()); - - let result = tera - .render_str("{{ os_family() }}", &Context::default()) - .unwrap(); - - assert_eq!("Linux", result); - } - - #[test] - #[cfg(target_family = "windows")] - fn test_render_with_custom_function_os_windows() { - let mut tera = get_tera(Option::default()); - - let result = tera - .render_str("{{ os_family() }}", &Context::default()) - .unwrap(); - - assert_eq!("Windows", result); + fn test_render_with_custom_function_os() { + reset(); + if cfg!(target_os = "linux") { + assert_eq!(render("{{os()}}"), "linux"); + } else if cfg!(target_os = "macos") { + assert_eq!(render("{{os()}}"), "macos"); + } else if cfg!(target_os = "windows") { + assert_eq!(render("{{os()}}"), "windows"); + } } #[test] - #[cfg(target_family = "unix")] - fn test_render_with_custom_function_invocation_directory() { - let a = env::set_current_dir("/tmp").is_ok(); - let mut tera = get_tera(Option::default()); - assert!(a); - println!("{:?}", env::current_dir().unwrap()); - - let result = tera - .render_str("{{ invocation_directory() }}", &Context::default()) - .unwrap(); - - assert_eq!("/tmp", result); - } - - #[test] - #[cfg(target_family = "windows")] - fn test_render_with_custom_function_invocation_directory() { - let a = env::set_current_dir("C:\\").is_ok(); - let mut tera = get_tera(Option::default()); - assert!(a); - - let result = tera - .render_str("{{ invocation_directory() }}", &Context::default()) - .unwrap(); - - assert_eq!("C:\\", result); + fn test_render_with_custom_function_os_family() { + reset(); + if cfg!(target_family = "unix") { + assert_eq!(render("{{os_family()}}"), "unix"); + } else if cfg!(target_os = "windows") { + assert_eq!(render("{{os_family()}}"), "windows"); + } } #[test] fn test_render_with_custom_filter_quote() { + reset(); let mut tera = get_tera(Option::default()); let result = tera @@ -314,6 +330,7 @@ mod tests { #[test] fn test_render_with_custom_filter_kebabcase() { + reset(); let mut tera = get_tera(Option::default()); let result = tera @@ -325,6 +342,7 @@ mod tests { #[test] fn test_render_with_custom_filter_lowercamelcase() { + reset(); let mut tera = get_tera(Option::default()); let result = tera @@ -336,6 +354,7 @@ mod tests { #[test] fn test_render_with_custom_filter_shoutykebabcase() { + reset(); let mut tera = get_tera(Option::default()); let result = tera @@ -347,6 +366,7 @@ mod tests { #[test] fn test_render_with_custom_filter_shoutysnakecase() { + reset(); let mut tera = get_tera(Option::default()); let result = tera @@ -358,6 +378,7 @@ mod tests { #[test] fn test_render_with_custom_filter_snakecase() { + reset(); let mut tera = get_tera(Option::default()); let result = tera @@ -369,6 +390,7 @@ mod tests { #[test] fn test_render_with_custom_filter_uppercamelcase() { + reset(); let mut tera = get_tera(Option::default()); let result = tera @@ -377,4 +399,80 @@ mod tests { assert_eq!("CamelCase", result); } + + #[test] + fn test_hash() { + reset(); + let s = render("{{ \"foo\" | hash(len=8) }}"); + assert_eq!(s, "2c26b46b"); + } + + #[test] + fn test_hash_file() { + reset(); + let s = render("{{ \"../fixtures/shorthands.toml\" | hash_file(len=64) }}"); + assert_snapshot!(s, @"518349c5734814ff9a21ab8d00ed2da6464b1699910246e763a4e6d5feb139fa"); + } + + #[test] + fn test_dirname() { + reset(); + let s = render(r#"{{ "a/b/c" | dirname }}"#); + assert_eq!(s, "a/b"); + } + + #[test] + fn test_basename() { + reset(); + let s = render(r#"{{ "a/b/c" | basename }}"#); + assert_eq!(s, "c"); + } + + #[test] + fn test_extname() { + reset(); + let s = render(r#"{{ "a/b/c.txt" | extname }}"#); + assert_eq!(s, "txt"); + } + + #[test] + fn test_file_stem() { + reset(); + let s = render(r#"{{ "a/b/c.txt" | file_stem }}"#); + assert_eq!(s, "c"); + } + + #[test] + fn test_file_size() { + reset(); + let s = render(r#"{{ "../fixtures/shorthands.toml" | file_size }}"#); + assert_eq!(s, "48"); + } + + #[test] + fn test_is_dir() { + reset(); + let s = render(r#"{% set p = ".mise" %}{% if p is dir %} ok {% endif %}"#); + assert_eq!(s.trim(), "ok"); + } + + #[test] + fn test_is_file() { + reset(); + let s = render(r#"{% set p = ".test-tool-versions" %}{% if p is file %} ok {% endif %}"#); + assert_eq!(s.trim(), "ok"); + } + + #[test] + fn test_exists() { + reset(); + let s = render(r#"{% set p = ".test-tool-versions" %}{% if p is exists %} ok {% endif %}"#); + assert_eq!(s.trim(), "ok"); + } + + fn render(s: &str) -> String { + let mut tera = get_tera(Option::default()); + + tera.render_str(s, &Context::default()).unwrap() + } }