From afd0c64da20c07c5c45e238510d7cc93201e4742 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:40:04 -0600 Subject: [PATCH 1/3] env-man --- src/cli/hook_env.rs | 15 +- src/cli/set.rs | 2 +- src/config/config_file/mise_toml.rs | 687 +++++++++++------- src/config/config_file/mod.rs | 21 +- ...config_file__mise_toml__tests__env-2.snap} | 3 - ..._config_file__mise_toml__tests__env-3.snap | 7 +- ..._config_file__mise_toml__tests__env-4.snap | 2 +- ..._config_file__mise_toml__tests__env-5.snap | 10 - ...fig_file__mise_toml__tests__fixture-5.snap | 11 +- ...onfig_file__mise_toml__tests__fixture.snap | 11 +- ...g_file__mise_toml__tests__path_dirs-4.snap | 5 - ...g_file__mise_toml__tests__path_dirs-5.snap | 13 - src/config/config_file/tool_versions.rs | 2 +- src/config/env_directive.rs | 146 ++++ src/config/mod.rs | 110 +-- src/dirs.rs | 1 + src/hook_env.rs | 14 +- src/plugins/external_plugin_cache.rs | 3 +- src/task.rs | 2 +- src/tera.rs | 13 +- src/toolset/builder.rs | 4 +- src/toolset/mod.rs | 4 +- test/fixtures/.env | 1 + test/fixtures/.env2 | 1 + 24 files changed, 689 insertions(+), 399 deletions(-) rename src/config/config_file/snapshots/{mise__config__config_file__mise_toml__tests__path_dirs-3.snap => mise__config__config_file__mise_toml__tests__env-2.snap} (63%) delete mode 100644 src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-5.snap delete mode 100644 src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-4.snap delete mode 100644 src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-5.snap create mode 100644 src/config/env_directive.rs create mode 100644 test/fixtures/.env create mode 100644 test/fixtures/.env2 diff --git a/src/cli/hook_env.rs b/src/cli/hook_env.rs index 712922d3d2..e2132ef330 100644 --- a/src/cli/hook_env.rs +++ b/src/cli/hook_env.rs @@ -1,6 +1,6 @@ use std::env::{join_paths, split_paths}; use std::ops::Deref; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use console::truncate_str; use eyre::Result; @@ -35,11 +35,7 @@ pub struct HookEnv { impl HookEnv { pub fn run(self) -> Result<()> { let config = Config::try_get()?; - let watch_files: Vec<_> = config - .config_files - .values() - .flat_map(|p| p.watch_files()) - .collect(); + let watch_files = config.watch_files()?; if hook_env::should_exit_early(&watch_files) { return Ok(()); } @@ -51,7 +47,7 @@ impl HookEnv { let mut diff = EnvDiff::new(&env::PRISTINE_ENV, env); let mut patches = diff.to_patches(); - let mut paths = config.path_dirs.clone(); + let mut paths = config.path_dirs()?.clone(); if let Some(p) = env_path { paths.extend(split_paths(&p).collect_vec()); } @@ -165,7 +161,10 @@ impl HookEnv { )) } - fn build_watch_operation(&self, watch_files: &[PathBuf]) -> Result { + fn build_watch_operation( + &self, + watch_files: impl IntoIterator>, + ) -> Result { let watches = hook_env::build_watches(watch_files)?; Ok(EnvDiffOperation::Add( "__MISE_WATCH".into(), diff --git a/src/cli/set.rs b/src/cli/set.rs index 65667a2ec7..31e147ed5b 100644 --- a/src/cli/set.rs +++ b/src/cli/set.rs @@ -50,7 +50,7 @@ impl Set { .map(|(key, (value, source))| Row { key: key.clone(), value: value.clone(), - source: display_path(source), + source: display_path(&source), }) .collect::>(); let mut table = tabled::Table::new(rows); diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index 89bb074034..b055050f7f 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -1,18 +1,21 @@ use std::collections::HashMap; use std::fmt::{Debug, Formatter}; -use std::iter::once; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::sync::Mutex; -use eyre::{Result, WrapErr}; -use serde::Deserializer; +use eyre::WrapErr; +use itertools::Itertools; +use serde::de::Visitor; +use serde::{de, Deserializer}; use serde_derive::Deserialize; use tera::Context as TeraContext; -use toml_edit::{table, value, Array, Document, Item, TableLike, Value}; +use toml_edit::{table, value, Array, Document, Item, Value}; use versions::Versioning; use crate::cli::args::ForgeArg; use crate::config::config_file::{trust_check, ConfigFile, ConfigFileType}; +use crate::config::env_directive::EnvDirective; use crate::config::AliasMap; use crate::file::{create_dir_all, display_path}; use crate::task::Task; @@ -24,6 +27,7 @@ use crate::ui::style; use crate::{dirs, file, parse_error}; #[derive(Default, Deserialize)] +// #[serde(deny_unknown_fields)] pub struct MiseToml { #[serde(default, deserialize_with = "deserialize_version")] min_version: Option, @@ -33,14 +37,12 @@ pub struct MiseToml { path: PathBuf, #[serde(skip)] toolset: Toolset, - #[serde(skip)] - env_files: Vec, - #[serde(skip)] - env: HashMap, - #[serde(skip)] - env_remove: Vec, - #[serde(skip)] - path_dirs: Vec, + #[serde(default, alias = "dotenv", deserialize_with = "deserialize_arr")] + env_file: Vec, + #[serde(default)] + env: EnvList, + #[serde(default, deserialize_with = "deserialize_arr")] + env_path: Vec, #[serde(skip)] alias: AliasMap, #[serde(skip)] @@ -51,12 +53,38 @@ pub struct MiseToml { tasks: Vec, #[serde(skip)] is_trusted: Mutex>, + #[serde(skip)] + project_root: Option, + #[serde(skip)] + config_root: PathBuf, } +#[derive(Debug, Default, Clone)] +pub struct EnvList(pub(crate) Vec); + impl MiseToml { pub fn init(path: &Path) -> Self { let mut context = BASE_CONTEXT.clone(); context.insert("config_root", path.parent().unwrap().to_str().unwrap()); + let filename = path.file_name().unwrap_or_default().to_string_lossy(); + let project_root = match path.parent() { + Some(dir) => match dir { + dir if dir.starts_with(*dirs::CONFIG) => None, + dir if dir.starts_with(*dirs::SYSTEM) => None, + dir if dir == *dirs::HOME => None, + dir if !filename.starts_with('.') && dir.ends_with(".mise") => dir.parent(), + dir if !filename.starts_with('.') && dir.ends_with(".config/mise") => { + dir.parent().unwrap().parent() + } + dir => Some(dir), + }, + None => None, + }; + let config_root = project_root + .or_else(|| path.parent()) + .or_else(|| dirs::CWD.as_ref().map(|p| p.as_path())) + .unwrap_or_else(|| *dirs::HOME) + .to_path_buf(); Self { path: path.to_path_buf(), context, @@ -65,11 +93,13 @@ impl MiseToml { source: Some(ToolSource::MiseToml(path.to_path_buf())), ..Default::default() }, + project_root: project_root.map(|p| p.to_path_buf()), + config_root, ..Default::default() } } - pub fn from_file(path: &Path) -> Result { + pub fn from_file(path: &Path) -> eyre::Result { trace!("parsing: {}", display_path(path)); let mut rf = Self::init(path); let body = file::read_to_string(path)?; // .suggestion("ensure file exists and can be read")?; @@ -78,9 +108,12 @@ impl MiseToml { Ok(rf) } - fn parse(&mut self, s: &str) -> Result<()> { + fn parse(&mut self, s: &str) -> eyre::Result<()> { let cfg: MiseToml = toml::from_str(s)?; self.min_version = cfg.min_version; + self.env = cfg.env; + self.env_file = cfg.env_file; + self.env_path = cfg.env_path; // TODO: right now some things are parsed with serde (above) and some not (below) everything // should be moved to serde eventually @@ -88,15 +121,11 @@ impl MiseToml { let doc: Document = s.parse()?; // .suggestion("ensure file is valid TOML")?; for (k, v) in doc.iter() { match k { - "dotenv" => self.parse_env_file(k, v)?, - "env_file" => self.parse_env_file(k, v)?, - "env_path" => self.path_dirs = self.parse_path_env(k, v)?, - "env" => self.parse_env(k, v)?, "alias" => self.alias = self.parse_alias(k, v)?, "tools" => self.toolset = self.parse_toolset(k, v)?, "plugins" => self.plugins = self.parse_plugins(k, v)?, "tasks" => self.tasks = self.parse_tasks(k, v)?, - "min_version" | "settings" => {} + "dotenv" | "env_file" | "env_path" | "min_version" | "settings" | "env" => {} _ => bail!("unknown key: {}", style::ered(k)), } } @@ -104,135 +133,7 @@ impl MiseToml { Ok(()) } - fn parse_env_file(&mut self, k: &str, v: &Item) -> Result<()> { - trust_check(&self.path)?; - if let Some(filename) = v.as_str() { - let path = self.path.parent().unwrap().join(filename); - self.parse_env_filename(path)?; - } else if let Some(filenames) = v.as_array() { - for filename in filenames { - if let Some(filename) = filename.as_str() { - let path = self.path.parent().unwrap().join(filename); - self.parse_env_filename(path)?; - } else { - parse_error!(k, v, "string"); - } - } - } else { - parse_error!(k, v, "string or array of strings"); - } - Ok(()) - } - - fn parse_env_filename(&mut self, path: PathBuf) -> Result<()> { - let dotenv = dotenvy::from_path_iter(&path) - .wrap_err_with(|| format!("failed to parse dotenv file: {}", display_path(&path)))?; - for item in dotenv { - let (k, v) = item?; - self.env.insert(k, v); - } - self.env_files.push(path); - Ok(()) - } - - fn parse_env(&mut self, key: &str, v: &Item) -> Result<()> { - trust_check(&self.path)?; - - if let Some(arr) = v.as_array_of_tables() { - arr.iter() - .try_for_each(|table| self.parse_single_env(key, table)) - } else if let Some(table) = v.as_table() { - self.parse_single_env(key, table) - } else { - parse_error!(key, v, "table or array of tables") - } - } - - fn parse_single_env(&mut self, key: &str, table: &dyn TableLike) -> Result<()> { - ensure!( - !table.contains_key("PATH"), - "use 'env.mise.path' instead of 'env.PATH'" - ); - - for (k, v) in table.iter() { - let key = format!("{}.{}", key, k); - let k = self.parse_template(&key, k)?; - if k == "_" || k == "mise" { - self.parse_env_mise(&key, v)?; - } else if let Some(v) = v.as_str() { - let v = self.parse_template(&key, v)?; - - if self.env.insert(k, v).is_some() { - bail!("duplicate key: {}", crate::ui::style::eyellow(key),) - } - } else if let Some(v) = v.as_integer() { - if self.env.insert(k, v.to_string()).is_some() { - bail!("duplicate key: {}", crate::ui::style::eyellow(key),) - } - } else if let Some(v) = v.as_bool() { - if self.env_remove.contains(&k) { - bail!("duplicate key: {}", crate::ui::style::eyellow(key),) - }; - - if !v { - self.env_remove.push(k); - } - } else { - parse_error!(key, v, "string, num, or bool") - } - } - - Ok(()) - } - - fn parse_env_mise(&mut self, key: &str, v: &Item) -> Result<()> { - match v.as_table_like() { - Some(table) => { - for (k, v) in table.iter() { - let key = format!("{}.{}", key, k); - match k { - "file" => self.parse_env_file(&key, v)?, - "path" => self.path_dirs = self.parse_path_env(&key, v)?, - _ => parse_error!(key, v, "file or path"), - } - } - Ok(()) - } - _ => parse_error!(key, v, "table"), - } - } - - fn parse_path_env(&self, k: &str, v: &Item) -> Result> { - trust_check(&self.path)?; - let config_root = self.path.parent().unwrap().to_path_buf(); - let get_path = |v: &Item| -> Result { - if let Some(s) = v.as_str() { - let s = self.parse_template(k, s)?; - let s = match s.strip_prefix("./") { - Some(s) => config_root.join(s), - None => match s.strip_prefix("~/") { - Some(s) => dirs::HOME.join(s), - None => s.into(), - }, - }; - Ok(s) - } else { - parse_error!(k, v, "string") - } - }; - if let Some(array) = v.as_array() { - let mut path = Vec::new(); - for v in array { - let item = Item::Value(v.clone()); - path.push(get_path(&item)?); - } - Ok(path) - } else { - Ok(vec![get_path(v)?]) - } - } - - fn parse_alias(&self, k: &str, v: &Item) -> Result { + fn parse_alias(&self, k: &str, v: &Item) -> eyre::Result { match v.as_table_like() { Some(table) => { let mut aliases = AliasMap::new(); @@ -262,12 +163,12 @@ impl MiseToml { } } - fn parse_plugins(&self, key: &str, v: &Item) -> Result> { + fn parse_plugins(&self, key: &str, v: &Item) -> eyre::Result> { trust_check(&self.path)?; self.parse_hashmap(key, v) } - fn parse_tasks(&self, key: &str, v: &Item) -> Result> { + fn parse_tasks(&self, key: &str, v: &Item) -> eyre::Result> { match v.as_table_like() { Some(table) => { let mut tasks = Vec::new(); @@ -283,7 +184,7 @@ impl MiseToml { } } - fn parse_task(&self, key: &str, v: &Item, name: &str) -> Result { + fn parse_task(&self, key: &str, v: &Item, name: &str) -> eyre::Result { let mut task = Task::new(name.into(), self.path.clone()); if v.as_str().is_some() { task.run = self.parse_string_or_array(key, v)?; @@ -322,7 +223,7 @@ impl MiseToml { } } - fn parse_hashmap(&self, key: &str, v: &Item) -> Result> { + fn parse_hashmap(&self, key: &str, v: &Item) -> eyre::Result> { match v.as_table_like() { Some(table) => { let mut env = HashMap::new(); @@ -342,7 +243,7 @@ impl MiseToml { } } - fn parse_toolset(&self, key: &str, v: &Item) -> Result { + fn parse_toolset(&self, key: &str, v: &Item) -> eyre::Result { let mut toolset = Toolset::new(self.toolset.source.clone().unwrap()); match v.as_table_like() { @@ -364,7 +265,7 @@ impl MiseToml { key: &str, v: &Item, fa: ForgeArg, - ) -> Result { + ) -> eyre::Result { let source = ToolSource::MiseToml(self.path.clone()); let mut tool_version_list = ToolVersionList::new(fa.clone(), source); @@ -409,7 +310,7 @@ impl MiseToml { key: &str, v: &Item, fa: ForgeArg, - ) -> Result<(ToolVersionRequest, ToolVersionOptions)> { + ) -> eyre::Result<(ToolVersionRequest, ToolVersionOptions)> { match v.as_table_like() { Some(table) => { let tv = if let Some(v) = table.get("version") { @@ -475,7 +376,7 @@ impl MiseToml { key: &str, v: &Value, fa: ForgeArg, - ) -> Result { + ) -> eyre::Result { match v.as_str() { Some(s) => { let s = self.parse_template(key, s)?; @@ -521,21 +422,21 @@ impl MiseToml { } } - fn parse_bool(&self, k: &str, v: &Item) -> Result { + fn parse_bool(&self, k: &str, v: &Item) -> eyre::Result { match v.as_value().map(|v| v.as_bool()) { Some(Some(v)) => Ok(v), _ => parse_error!(k, v, "boolean"), } } - fn parse_string(&self, k: &str, v: &Item) -> Result { + fn parse_string(&self, k: &str, v: &Item) -> eyre::Result { match v.as_value().map(|v| v.as_str()) { Some(Some(v)) => Ok(v.to_string()), _ => parse_error!(k, v, "string"), } } - fn parse_path(&self, k: &str, v: &Item) -> Result { + fn parse_path(&self, k: &str, v: &Item) -> eyre::Result { match v.as_value().map(|v| v.as_str()) { Some(Some(v)) => { let v = self.parse_template(k, v)?; @@ -545,7 +446,7 @@ impl MiseToml { } } - fn parse_string_or_array(&self, k: &str, v: &Item) -> Result> { + fn parse_string_or_array(&self, k: &str, v: &Item) -> eyre::Result> { match v.as_value().map(|v| v.as_str()) { Some(Some(v)) => { let v = self.parse_template(k, v)?; @@ -555,7 +456,7 @@ impl MiseToml { } } - fn parse_string_array(&self, k: &str, v: &Item) -> Result> { + fn parse_string_array(&self, k: &str, v: &Item) -> eyre::Result> { match v .as_array() .map(|v| v.iter().map(|v| v.as_str().unwrap().to_string()).collect()) @@ -585,12 +486,12 @@ impl MiseToml { env_tbl.remove(key); } - fn parse_template(&self, k: &str, input: &str) -> Result { + fn parse_template(&self, k: &str, input: &str) -> eyre::Result { if !input.contains("{{") && !input.contains("{%") && !input.contains("{#") { return Ok(input.to_string()); } trust_check(&self.path)?; - let dir = self.path.parent().unwrap(); + let dir = self.path.parent(); let output = get_tera(dir) .render_str(input, &self.context) .wrap_err_with(|| eyre!("failed to parse template: {k}='{input}'"))?; @@ -602,7 +503,6 @@ impl ConfigFile for MiseToml { fn get_type(&self) -> ConfigFileType { ConfigFileType::MiseToml } - fn get_path(&self) -> &Path { self.path.as_path() } @@ -612,37 +512,30 @@ impl ConfigFile for MiseToml { } fn project_root(&self) -> Option<&Path> { - let fp = self.get_path(); - let filename = fp.file_name().unwrap_or_default().to_string_lossy(); - match fp.parent() { - Some(dir) => match dir { - dir if dir.starts_with(*dirs::CONFIG) => None, - dir if dir.starts_with(*dirs::SYSTEM) => None, - dir if dir == *dirs::HOME => None, - dir if !filename.starts_with('.') && dir.ends_with(".mise") => dir.parent(), - dir if !filename.starts_with('.') && dir.ends_with(".config/mise") => { - dir.parent().unwrap().parent() - } - dir => Some(dir), - }, - None => None, - } + self.project_root.as_deref() } fn plugins(&self) -> HashMap { self.plugins.clone() } - fn env(&self) -> HashMap { - self.env.clone() - } - - fn env_remove(&self) -> Vec { - self.env_remove.clone() - } - - fn env_path(&self) -> Vec { - self.path_dirs.clone() + fn env_entries(&self) -> Vec { + let env_entries = self.env.0.iter().cloned(); + let path_entries = self + .env_path + .iter() + .map(|p| EnvDirective::Path(p.clone())) + .collect_vec(); + let env_files = self + .env_file + .iter() + .map(|p| EnvDirective::File(p.clone())) + .collect_vec(); + path_entries + .into_iter() + .chain(env_files) + .chain(env_entries) + .collect() } fn tasks(&self) -> Vec<&Task> { @@ -686,7 +579,7 @@ impl ConfigFile for MiseToml { } } - fn save(&self) -> Result<()> { + fn save(&self) -> eyre::Result<()> { let contents = self.dump(); if let Some(parent) = self.path.parent() { create_dir_all(parent)?; @@ -705,13 +598,6 @@ impl ConfigFile for MiseToml { fn aliases(&self) -> AliasMap { self.alias.clone() } - - fn watch_files(&self) -> Vec { - once(&self.path) - .chain(self.env_files.iter()) - .cloned() - .collect() - } } impl Debug for MiseToml { @@ -723,17 +609,12 @@ impl Debug for MiseToml { if let Some(min_version) = &self.min_version { d.field("min_version", &min_version.to_string()); } - if !self.env_files.is_empty() { - d.field("env_files", &self.env_files); + if !self.env_file.is_empty() { + d.field("env_file", &self.env_file); } - if !self.env.is_empty() { - d.field("env", &self.env); - } - if !self.env_remove.is_empty() { - d.field("env_remove", &self.env_remove); - } - if !self.path_dirs.is_empty() { - d.field("path_dirs", &self.path_dirs); + let env = self.env_entries(); + if !env.is_empty() { + d.field("env", &env); } if !self.alias.is_empty() { d.field("alias", &self.alias); @@ -752,15 +633,16 @@ impl Clone for MiseToml { context: self.context.clone(), path: self.path.clone(), toolset: self.toolset.clone(), - env_files: self.env_files.clone(), + env_file: self.env_file.clone(), env: self.env.clone(), - env_remove: self.env_remove.clone(), - path_dirs: self.path_dirs.clone(), + env_path: self.env_path.clone(), alias: self.alias.clone(), doc: self.doc.clone(), plugins: self.plugins.clone(), tasks: self.tasks.clone(), is_trusted: Mutex::new(*self.is_trusted.lock().unwrap()), + project_root: self.project_root.clone(), + config_root: self.config_root.clone(), } } } @@ -781,6 +663,178 @@ where } } +fn deserialize_arr<'de, D, T>(deserializer: D) -> eyre::Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr, + ::Err: std::fmt::Display, +{ + struct ArrVisitor(std::marker::PhantomData); + + impl<'de, T> Visitor<'de> for ArrVisitor + where + T: FromStr, + ::Err: std::fmt::Display, + { + type Value = Vec; + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("string or array of strings") + } + + fn visit_str(self, v: &str) -> std::result::Result + where + E: de::Error, + { + let v = v.parse().map_err(de::Error::custom)?; + Ok(vec![v]) + } + + fn visit_seq(self, mut seq: S) -> std::result::Result + where + S: de::SeqAccess<'de>, + { + let mut v = vec![]; + while let Some(s) = seq.next_element::()? { + v.push(s.parse().map_err(de::Error::custom)?); + } + Ok(v) + } + } + + deserializer.deserialize_any(ArrVisitor(std::marker::PhantomData)) +} + +impl<'de> de::Deserialize<'de> for EnvList { + fn deserialize(deserializer: D) -> std::result::Result + where + D: de::Deserializer<'de>, + { + struct EnvManVisitor; + + impl<'de> Visitor<'de> for EnvManVisitor { + type Value = EnvList; + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("env table or array of env tables") + } + + fn visit_seq(self, mut seq: S) -> std::result::Result + where + S: de::SeqAccess<'de>, + { + let mut env = vec![]; + while let Some(list) = seq.next_element::()? { + env.extend(list.0); + } + Ok(EnvList(env)) + } + fn visit_map(self, mut map: M) -> std::result::Result + where + M: de::MapAccess<'de>, + { + let mut env = vec![]; + while let Some(key) = map.next_key::()? { + match key.as_str() { + "_" | "mise" => { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct EnvDirectives { + #[serde(default, deserialize_with = "deserialize_arr")] + path: Vec, + #[serde(default, deserialize_with = "deserialize_arr")] + file: Vec, + #[serde(default, deserialize_with = "deserialize_arr")] + source: Vec, + } + let directives = map.next_value::()?; + // TODO: parse these in the order they're defined somehow + for path in directives.path { + env.push(EnvDirective::Path(path)); + } + for file in directives.file { + env.push(EnvDirective::File(file)); + } + for source in directives.source { + env.push(EnvDirective::Source(source)); + } + } + _ => { + enum Val { + Int(i64), + Str(String), + Bool(bool), + } + + impl<'de> de::Deserialize<'de> for Val { + fn deserialize( + deserializer: D, + ) -> std::result::Result + where + D: de::Deserializer<'de>, + { + struct ValVisitor; + + impl<'de> Visitor<'de> for ValVisitor { + type Value = Val; + fn expecting( + &self, + formatter: &mut Formatter, + ) -> std::fmt::Result + { + formatter.write_str("env value") + } + + fn visit_bool(self, v: bool) -> Result + where + E: de::Error, + { + match v { + true => Err(de::Error::custom( + "env values cannot be true", + )), + false => Ok(Val::Bool(v)), + } + } + + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + Ok(Val::Int(v)) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(Val::Str(v.to_string())) + } + } + + deserializer.deserialize_any(ValVisitor) + } + } + + let value = map.next_value::()?; + match value { + Val::Int(i) => { + env.push(EnvDirective::Val(key, i.to_string())); + } + Val::Str(s) => { + env.push(EnvDirective::Val(key, s)); + } + Val::Bool(_) => env.push(EnvDirective::Rm(key)), + } + } + } + } + Ok(EnvList(env)) + } + } + + deserializer.deserialize_any(EnvManVisitor) + } +} + #[cfg(test)] mod tests { use crate::dirs; @@ -792,7 +846,7 @@ mod tests { fn test_fixture() { let cf = MiseToml::from_file(&dirs::HOME.join("fixtures/.mise.toml")).unwrap(); - assert_debug_snapshot!(cf.env()); + assert_debug_snapshot!(cf.env_entries()); assert_debug_snapshot!(cf.plugins()); assert_snapshot!(replace_path(&format!("{:#?}", cf.toolset))); assert_debug_snapshot!(cf.alias); @@ -802,20 +856,14 @@ mod tests { #[test] fn test_env() { - let mut cf = MiseToml::init(PathBuf::from("/tmp/.mise.toml").as_path()); - cf.parse(&formatdoc! {r#" + let cf = MiseToml::init(PathBuf::from("/tmp/.mise.toml").as_path()); + let env = parse_env(formatdoc! {r#" min_version = "2024.1.1" [env] foo="bar" - "#}) - .unwrap(); + "#}); - assert_debug_snapshot!(cf.env(), @r###" - { - "foo": "bar", - } - "###); - assert_debug_snapshot!(cf.env_path(), @"[]"); + assert_debug_snapshot!(env, @r###""foo=bar""###); let cf: Box = Box::new(cf); with_settings!({ assert_snapshot!(cf.dump()); @@ -826,65 +874,107 @@ mod tests { #[test] fn test_env_array_valid() { - let mut cf = MiseToml::init(PathBuf::from("/tmp/.mise.toml").as_path()); - cf.parse(&formatdoc! {r#" + let env = parse_env(formatdoc! {r#" [[env]] foo="bar" [[env]] bar="baz" - "#}) - .unwrap(); + "#}); - assert_snapshot!(cf.env()["foo"], @"bar"); - assert_snapshot!(cf.env()["bar"], @"baz"); + assert_snapshot!(env, @r###" + foo=bar + bar=baz + "###); } #[test] - fn test_env_array_duplicate_key() { - let inputs = vec![(r#""bar""#, r#""baz""#), ("1", "2"), ("false", "true")]; + fn test_path_dirs() { + let env = parse_env(formatdoc! {r#" + env_path=["/foo", "./bar"] + [env] + foo="bar" + "#}); - for (a, b) in inputs { - let mut cf = MiseToml::init(PathBuf::from("/tmp/.mise.toml").as_path()); - let err = cf - .parse(&formatdoc! {r#" - [[env]] - foo={a} + assert_snapshot!(env, @r###" + path_add /foo + path_add ./bar + foo=bar + "###); + + let env = parse_env(formatdoc! {r#" + env_path="./bar" + "#}); + assert_snapshot!(env, @"path_add ./bar"); + let env = parse_env(formatdoc! {r#" + [env] + _.path = "./bar" + "#}); + assert_debug_snapshot!(env, @r###""path_add ./bar""###); + + let env = parse_env(formatdoc! {r#" + [env] + _.path = ["/foo", "./bar"] + "#}); + assert_snapshot!(env, @r###" + path_add /foo + path_add ./bar + "###); + + let env = parse_env(formatdoc! {r#" + [[env]] + _.path = "/foo" [[env]] - foo={b} - "#}) - .unwrap_err(); + _.path = "./bar" + "#}); + assert_snapshot!(env, @r###" + path_add /foo + path_add ./bar + "###); - allow_duplicates!( - assert_snapshot!(err.to_string(), @"duplicate key: env.foo"); - ) - } + let env = parse_env(formatdoc! {r#" + env_path = "/foo" + [env] + _.path = "./bar" + "#}); + assert_snapshot!(env, @r###" + path_add /foo + path_add ./bar + "###); } #[test] - fn test_path_dirs() { - with_settings!({ - let p = dirs::HOME.join("fixtures/.mise.toml"); - let mut cf = MiseToml::init(&p); - cf.parse(&formatdoc! {r#" - env_path=["/foo", "./bar"] + fn test_env_file() { + let env = parse_env(formatdoc! {r#" + env_file = ".env" + "#}); + + assert_debug_snapshot!(env, @r###""dotenv .env""###); + + let env = parse_env(formatdoc! {r#" + env_file=[".env", ".env2"] + "#}); + assert_debug_snapshot!(env, @r###""dotenv .env\ndotenv .env2""###); + + let env = parse_env(formatdoc! {r#" [env] - foo="bar" - "#}) - .unwrap(); + _.file = ".env" + "#}); + assert_debug_snapshot!(env, @r###""dotenv .env""###); - assert_debug_snapshot!(cf.env(), @r###" - { - "foo": "bar", - } - "###); - assert_snapshot!(replace_path(&format!("{:?}", cf.env_path())), @r###"["/foo", "~/fixtures/bar"]"###); - let cf: Box = Box::new(cf); - assert_snapshot!(cf.dump()); - assert_display_snapshot!(cf); - assert_debug_snapshot!(cf); - }); + let env = parse_env(formatdoc! {r#" + [env] + _.file = [".env", ".env2"] + "#}); + assert_debug_snapshot!(env, @r###""dotenv .env\ndotenv .env2""###); + + let env = parse_env(formatdoc! {r#" + dotenv = ".env" + [env] + _.file = ".env2" + "#}); + assert_debug_snapshot!(env, @r###""dotenv .env\ndotenv .env2""###); } #[test] @@ -970,11 +1060,80 @@ mod tests { #[test] fn test_fail_with_unknown_key() { let mut cf = MiseToml::init(PathBuf::from("/tmp/.mise.toml").as_path()); - let err = cf + let _ = cf .parse(&formatdoc! {r#" invalid_key = true "#}) .unwrap_err(); - assert_snapshot!(err.to_string(), @"unknown key: invalid_key"); + } + + #[test] + fn test_env_entries() { + let toml = formatdoc! {r#" + [env] + foo1="1" + rm=false + _.path="/foo" + foo2="2" + _.file=".env" + foo3="3" + "#}; + assert_snapshot!(parse_env(toml), @r###" + foo1=1 + unset rm + path_add /foo + dotenv .env + foo2=2 + foo3=3 + "###); + } + + #[test] + fn test_env_arr() { + let toml = formatdoc! {r#" + [[env]] + foo1="1" + rm=false + _.path="/foo" + foo2="2" + _.file=".env" + foo3="3" + _.source="/baz1" + + [[env]] + foo4="4" + rm=false + _.file=".env2" + foo5="5" + _.path="/bar" + foo6="6" + _.source="/baz2" + "#}; + assert_snapshot!(parse_env(toml), @r###" + foo1=1 + unset rm + path_add /foo + dotenv .env + source /baz1 + foo2=2 + foo3=3 + foo4=4 + unset rm + path_add /bar + dotenv .env2 + source /baz2 + foo5=5 + foo6=6 + "###); + } + + fn parse(s: String) -> MiseToml { + let mut cf = MiseToml::init(PathBuf::from("/tmp/.mise.toml").as_path()); + cf.parse(&s).unwrap(); + cf + } + + fn parse_env(toml: String) -> String { + parse(toml).env_entries().into_iter().join("\n") } } diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 8b4f80bba9..5f3a6149a7 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -13,7 +13,8 @@ use tool_versions::ToolVersions; use crate::cli::args::{ForgeArg, ToolArg}; use crate::config::config_file::mise_toml::MiseToml; -use crate::config::{global_config_files, system_config_files, AliasMap, Settings}; +use crate::config::env_directive::EnvDirective; +use crate::config::{AliasMap, Settings}; use crate::errors::Error::UntrustedConfig; use crate::file::display_path; use crate::hash::{file_hash_sha256, hash_to_str}; @@ -62,14 +63,11 @@ pub trait ConfigFile: Debug + Send + Sync { fn plugins(&self) -> HashMap { Default::default() } - fn env(&self) -> HashMap { + fn env_entries(&self) -> Vec { Default::default() } - fn env_remove(&self) -> Vec { - Default::default() - } - fn env_path(&self) -> Vec { - Default::default() + fn env_paths(&self) -> Result> { + Ok(Default::default()) } fn tasks(&self) -> Vec<&Task> { Default::default() @@ -82,15 +80,6 @@ pub trait ConfigFile: Debug + Send + Sync { fn aliases(&self) -> AliasMap { Default::default() } - fn watch_files(&self) -> Vec { - vec![self.get_path().to_path_buf()] - } - fn is_global(&self) -> bool { - global_config_files() - .iter() - .chain(system_config_files().iter()) - .any(|p| p == self.get_path()) - } } impl dyn ConfigFile { diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-3.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-2.snap similarity index 63% rename from src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-3.snap rename to src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-2.snap index e71d936af7..a2587ae1e4 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-3.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-2.snap @@ -2,7 +2,4 @@ source: src/config/config_file/mise_toml.rs expression: cf.dump() --- -env_path=["/foo", "./bar"] -[env] -foo="bar" diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-3.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-3.snap index 3a69d8ca07..7f358280fc 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-3.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-3.snap @@ -1,8 +1,5 @@ --- source: src/config/config_file/mise_toml.rs -expression: cf.dump() +expression: cf --- -min_version = "2024.1.1" -[env] -foo="bar" - +/tmp/.mise.toml: diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-4.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-4.snap index 7f358280fc..11ebdc792b 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-4.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-4.snap @@ -2,4 +2,4 @@ source: src/config/config_file/mise_toml.rs expression: cf --- -/tmp/.mise.toml: +MiseToml(/tmp/.mise.toml): diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-5.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-5.snap deleted file mode 100644 index acaad84357..0000000000 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__env-5.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/config/config_file/mise_toml.rs -expression: cf ---- -MiseToml(/tmp/.mise.toml): { - min_version: "2024.1.1", - env: { - "foo": "bar", - }, -} diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture-5.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture-5.snap index a338ba5211..81c72851cf 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture-5.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture-5.snap @@ -1,11 +1,14 @@ --- source: src/config/config_file/mise_toml.rs -expression: "replace_path(&format!(\"{:#?}\", & cf))" +expression: "replace_path(&format!(\"{:#?}\", &cf))" --- MiseToml(~/fixtures/.mise.toml): terraform@1.0.0, node@18 node@prefix:20 node@ref:master node@path:~/.nodes/18, jq@prefix:1.6, shellcheck@0.9.0, python@3.10.0 python@3.9.0 { - env: { - "NODE_ENV": "production", - }, + env: [ + Val( + "NODE_ENV", + "production", + ), + ], alias: { ForgeArg("node"): { "my_custom_node": "18", diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture.snap index 48bcd6c18d..c39cbcaf0e 100644 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture.snap +++ b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__fixture.snap @@ -1,7 +1,10 @@ --- source: src/config/config_file/mise_toml.rs -expression: cf.env() +expression: cf.env_entries() --- -{ - "NODE_ENV": "production", -} +[ + Val( + "NODE_ENV", + "production", + ), +] diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-4.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-4.snap deleted file mode 100644 index 5ab096ef2c..0000000000 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-4.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/config/config_file/mise_toml.rs -expression: cf ---- -~/fixtures/.mise.toml: diff --git a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-5.snap b/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-5.snap deleted file mode 100644 index 7216a2ba06..0000000000 --- a/src/config/config_file/snapshots/mise__config__config_file__mise_toml__tests__path_dirs-5.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/config/config_file/mise_toml.rs -expression: cf ---- -MiseToml(~/fixtures/.mise.toml): { - env: { - "foo": "bar", - }, - path_dirs: [ - "/foo", - "~/fixtures/bar", - ], -} diff --git a/src/config/config_file/tool_versions.rs b/src/config/config_file/tool_versions.rs index e2d3df0e35..86b82ad00e 100644 --- a/src/config/config_file/tool_versions.rs +++ b/src/config/config_file/tool_versions.rs @@ -55,7 +55,7 @@ impl ToolVersions { pub fn parse_str(s: &str, path: PathBuf) -> Result { let mut cf = Self::init(&path); - let dir = path.parent().unwrap(); + let dir = path.parent(); let s = if config_file::is_trusted(&path) { get_tera(dir).render_str(s, &cf.context)? } else { diff --git a/src/config/env_directive.rs b/src/config/env_directive.rs new file mode 100644 index 0000000000..b5d7495e0f --- /dev/null +++ b/src/config/env_directive.rs @@ -0,0 +1,146 @@ +use crate::config::config_file::trust_check; +use crate::dirs; +use crate::file::display_path; +use crate::tera::{get_tera, BASE_CONTEXT}; +use eyre::Context; +use indexmap::IndexMap; +use std::collections::{BTreeSet, HashMap}; +use std::fmt::Display; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone)] +pub enum EnvDirective { + /// simple key/value pair + Val(String, String), + /// remove a key + Rm(String), + /// dotenv file + File(PathBuf), + /// add a path to the PATH + Path(PathBuf), + /// run a bash script and apply the resulting env diff + Source(PathBuf), +} + +impl From<(String, String)> for EnvDirective { + fn from((k, v): (String, String)) -> Self { + Self::Val(k, v) + } +} + +impl From<(String, i64)> for EnvDirective { + fn from((k, v): (String, i64)) -> Self { + (k, v.to_string()).into() + } +} + +impl Display for EnvDirective { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EnvDirective::Val(k, v) => write!(f, "{k}={v}"), + EnvDirective::Rm(k) => write!(f, "unset {k}"), + EnvDirective::File(path) => write!(f, "dotenv {}", display_path(path)), + EnvDirective::Path(path) => write!(f, "path_add {}", display_path(path)), + EnvDirective::Source(path) => write!(f, "source {}", display_path(path)), + } + } +} + +#[derive(Debug)] +pub struct EnvResults { + pub env: IndexMap, + pub env_remove: BTreeSet, + pub env_files: Vec, + pub env_paths: Vec, + pub env_scripts: Vec, +} + +impl EnvResults { + pub fn resolve( + initial: &HashMap, + input: Vec<(EnvDirective, PathBuf)>, + ) -> eyre::Result { + let mut ctx = BASE_CONTEXT.clone(); + let mut env = initial + .iter() + .map(|(k, v)| (k.clone(), (v.clone(), None))) + .collect::>(); + let mut r = Self { + env: Default::default(), + env_remove: BTreeSet::new(), + env_files: Vec::new(), + env_paths: Vec::new(), + env_scripts: Vec::new(), + }; + for (directive, source) in input { + let config_root = source.parent().unwrap(); + ctx.insert("config_root", config_root); + let normalize_path = |s: String| { + let s = s.strip_prefix("./").unwrap_or(&s); + match s.strip_prefix("~/") { + Some(s) => dirs::HOME.join(s), + None if s.starts_with('/') => PathBuf::from(s), + None => config_root.join(s), + } + }; + match directive { + EnvDirective::Val(k, v) => { + let v = r.parse_template(&ctx, &source, &v)?; + r.env_remove.remove(&k); + env.insert(k, (v, Some(source.clone()))); + } + EnvDirective::Rm(k) => { + env.remove(&k); + r.env_remove.insert(k); + } + EnvDirective::Path(input) => { + let s = r.parse_template(&ctx, &source, input.to_string_lossy().as_ref())?; + let p = normalize_path(s); + r.env_paths.push(p.clone()); + } + EnvDirective::File(input) => { + trust_check(&source)?; + let s = r.parse_template(&ctx, &source, input.to_string_lossy().as_ref())?; + let p = normalize_path(s); + r.env_files.push(p.clone()); + let errfn = || eyre!("failed to parse dotenv file: {}", display_path(&p)); + for item in dotenvy::from_path_iter(&p).wrap_err_with(errfn)? { + let (k, v) = item.wrap_err_with(errfn)?; + r.env_remove.remove(&k); + env.insert(k, (v, Some(p.clone()))); + } + } + EnvDirective::Source(input) => { + trust_check(&source)?; + let s = r.parse_template(&ctx, &source, input.to_string_lossy().as_ref())?; + let p = normalize_path(s); + r.env_scripts.push(p.clone()); + // TODO: run script and apply diff + } + }; + } + for (k, (v, source)) in env { + if let Some(source) = source { + r.env.insert(k, (v, source)); + } + } + Ok(r) + } + + fn parse_template( + &self, + ctx: &tera::Context, + path: &Path, + input: &str, + ) -> eyre::Result { + if !input.contains("{{") && !input.contains("{%") && !input.contains("{#") { + return Ok(input.to_string()); + } + trust_check(path)?; + let dir = path.parent(); + let output = get_tera(dir) + .render_str(input, ctx) + .wrap_err_with(|| eyre!("failed to parse template: '{input}'"))?; + Ok(output) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index a2b9302d91..d486788e68 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,7 +1,7 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::{Debug, Formatter}; use std::iter::once; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use either::Either; @@ -18,6 +18,7 @@ use crate::cli::version; use crate::config::config_file::legacy_version::LegacyVersionFile; use crate::config::config_file::mise_toml::MiseToml; use crate::config::config_file::ConfigFile; +use crate::config::env_directive::EnvResults; use crate::config::tracking::Tracker; use crate::file::display_path; use crate::forge::Forge; @@ -27,6 +28,7 @@ use crate::ui::style; use crate::{dirs, env, file, forge}; pub mod config_file; +mod env_directive; pub mod settings; mod tracking; @@ -38,9 +40,9 @@ type EnvWithSources = IndexMap; pub struct Config { pub aliases: AliasMap, pub config_files: ConfigMap, - pub path_dirs: Vec, pub project_root: Option, - env: OnceCell, + env: OnceCell, + env_with_sources: OnceCell, all_aliases: OnceCell, repo_urls: HashMap, shorthands: OnceCell>, @@ -79,7 +81,7 @@ impl Config { let config = Self { env: OnceCell::new(), - path_dirs: load_path_dirs(&config_files), + env_with_sources: OnceCell::new(), aliases: load_aliases(&config_files), all_aliases: OnceCell::new(), shorthands: OnceCell::new(), @@ -96,15 +98,39 @@ impl Config { Ok(config) } - pub fn env(&self) -> Result> { + pub fn env(&self) -> eyre::Result> { Ok(self .env_with_sources()? .iter() .map(|(k, (v, _))| (k.clone(), v.clone())) .collect()) } - pub fn env_with_sources(&self) -> Result<&EnvWithSources> { - self.env.get_or_try_init(|| load_env(&self.config_files)) + pub fn env_with_sources(&self) -> eyre::Result<&EnvWithSources> { + self.env_with_sources.get_or_try_init(|| { + let mut env = self.env_results()?.env.clone(); + let settings = Settings::get(); + if let Some(env_file) = &settings.env_file { + match dotenvy::from_filename_iter(env_file) { + Ok(iter) => { + for item in iter { + let (k, v) = item.unwrap_or_else(|err| { + warn!("env_file: {err}"); + Default::default() + }); + env.insert(k, (v, env_file.clone())); + } + } + Err(err) => trace!("env_file: {err}"), + } + } + Ok(env) + }) + } + pub fn env_results(&self) -> eyre::Result<&EnvResults> { + self.env.get_or_try_init(|| self.load_env()) + } + pub fn path_dirs(&self) -> eyre::Result<&Vec> { + Ok(&self.env_results()?.env_paths) } pub fn get_shorthands(&self) -> &Shorthands { self.shorthands @@ -284,6 +310,25 @@ impl Config { Ok(()) } + fn load_env(&self) -> Result { + let entries = self + .config_files + .iter() + .rev() + .flat_map(|(source, cf)| cf.env_entries().into_iter().map(|e| (e, source.clone()))) + .collect(); + EnvResults::resolve(&*env::PRISTINE_ENV, entries) + } + + pub fn watch_files(&self) -> eyre::Result> { + Ok(self + .config_files + .keys() + .chain(self.env_results()?.env_files.iter()) + .map(|p| p.as_path()) + .collect()) + } + #[cfg(test)] pub fn reset() { Settings::reset(None); @@ -390,6 +435,13 @@ pub fn load_config_paths(config_filenames: &[String]) -> Vec { config_files.into_iter().unique().collect() } +pub fn is_global_config(path: &Path) -> bool { + global_config_files() + .iter() + .chain(system_config_files().iter()) + .any(|p| p == path) +} + pub fn global_config_files() -> Vec { let mut config_files = vec![]; if env::var_path("MISE_CONFIG_FILE").is_none() @@ -463,43 +515,6 @@ fn parse_config_file( } } -fn load_env(config_files: &ConfigMap) -> Result { - let mut env = IndexMap::new(); - for (source, cf) in config_files.iter().rev() { - for (k, v) in cf.env() { - env.insert(k, (v, source.clone())); - } - for k in cf.env_remove() { - // remove values set to "false" - env.remove(&k); - } - } - let settings = Settings::get(); - if let Some(env_file) = &settings.env_file { - match dotenvy::from_filename_iter(env_file) { - Ok(iter) => { - for item in iter { - let (k, v) = item.unwrap_or_else(|err| { - warn!("env_file: {err}"); - Default::default() - }); - env.insert(k, (v, env_file.clone())); - } - } - Err(err) => trace!("env_file: {err}"), - } - } - Ok(env) -} - -fn load_path_dirs(config_files: &ConfigMap) -> Vec { - let mut path_dirs = vec![]; - for cf in config_files.values().rev() { - path_dirs.extend(cf.env_path()); - } - path_dirs -} - fn load_aliases(config_files: &ConfigMap) -> AliasMap { let mut aliases: AliasMap = AliasMap::new(); @@ -535,8 +550,9 @@ impl Debug for Config { // s.field("Env Sources", &self.env_sources); } } - if !self.path_dirs.is_empty() { - s.field("Path Dirs", &self.path_dirs); + let path_dirs = self.path_dirs().cloned().unwrap_or_default(); + if !path_dirs.is_empty() { + s.field("Path Dirs", &path_dirs); } if !self.aliases.is_empty() { s.field("Aliases", &self.aliases); diff --git a/src/dirs.rs b/src/dirs.rs index ede1d6e0ab..9ebb4f456c 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -5,6 +5,7 @@ use once_cell::sync::Lazy; use crate::env; pub static HOME: Lazy<&Path> = Lazy::new(|| &env::HOME); +pub static CWD: Lazy> = Lazy::new(|| env::current_dir().ok()); pub static DATA: Lazy<&Path> = Lazy::new(|| &env::MISE_DATA_DIR); pub static CACHE: Lazy<&Path> = Lazy::new(|| &env::MISE_CACHE_DIR); pub static CONFIG: Lazy<&Path> = Lazy::new(|| &env::MISE_CONFIG_DIR); diff --git a/src/hook_env.rs b/src/hook_env.rs index 4c56402dd7..2e9d737863 100644 --- a/src/hook_env.rs +++ b/src/hook_env.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::io::prelude::*; use std::ops::Deref; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::SystemTime; use base64::prelude::*; @@ -19,7 +19,7 @@ use crate::{dirs, env}; /// this function will early-exit the application if hook-env is being /// called and it does not need to be -pub fn should_exit_early(watch_files: &[PathBuf]) -> bool { +pub fn should_exit_early(watch_files: impl IntoIterator>) -> bool { let args = env::ARGS.read().unwrap(); if args.len() < 2 || args[1] != "hook-env" { return false; @@ -101,7 +101,9 @@ pub fn deserialize_watches(raw: String) -> Result { Ok(rmp_serde::from_slice(&writer[..])?) } -pub fn build_watches(watch_files: &[PathBuf]) -> Result { +pub fn build_watches( + watch_files: impl IntoIterator>, +) -> Result { let mut watches = BTreeMap::new(); for cf in get_watch_files(watch_files) { watches.insert(cf.clone(), cf.metadata()?.modified()?); @@ -113,13 +115,15 @@ pub fn build_watches(watch_files: &[PathBuf]) -> Result { }) } -pub fn get_watch_files(watch_files: &[PathBuf]) -> BTreeSet { +pub fn get_watch_files( + watch_files: impl IntoIterator>, +) -> BTreeSet { let mut watches = BTreeSet::new(); if dirs::DATA.exists() { watches.insert(dirs::DATA.to_path_buf()); } for cf in watch_files { - watches.insert(cf.clone()); + watches.insert(cf.as_ref().to_path_buf()); } watches diff --git a/src/plugins/external_plugin_cache.rs b/src/plugins/external_plugin_cache.rs index 5c79e70cfe..d37aedcbff 100644 --- a/src/plugins/external_plugin_cache.rs +++ b/src/plugins/external_plugin_cache.rs @@ -99,7 +99,8 @@ fn parse_template(config: &Config, tv: &ToolVersion, tmpl: &str) -> Result = Lazy::new(|| { context }); -pub fn get_tera(dir: &Path) -> Tera { +pub fn get_tera(dir: Option<&Path>) -> Tera { let mut tera = Tera::default(); - let dir = dir.to_path_buf(); + let dir = dir.map(PathBuf::from); tera.register_function( "exec", move |args: &HashMap| -> tera::Result { match args.get("command") { Some(Value::String(command)) => { - let result = cmd("bash", ["-c", command]) - .dir(&dir) - .full_env(&*env::PRISTINE_ENV) - .read()?; + let mut cmd = cmd("bash", ["-c", command]).full_env(&*env::PRISTINE_ENV); + if let Some(dir) = &dir { + cmd = cmd.dir(dir); + } + let result = cmd.read()?; Ok(Value::String(result)) } _ => Err("exec command must be a string".into()), diff --git a/src/toolset/builder.rs b/src/toolset/builder.rs index 4908931ce4..dd1b5bd702 100644 --- a/src/toolset/builder.rs +++ b/src/toolset/builder.rs @@ -5,8 +5,8 @@ use itertools::Itertools; use crate::cli::args::{ForgeArg, ToolArg}; use crate::config::{Config, Settings}; -use crate::env; use crate::toolset::{ToolSource, ToolVersionRequest, Toolset}; +use crate::{config, env}; #[derive(Debug, Default)] pub struct ToolsetBuilder { @@ -50,7 +50,7 @@ impl ToolsetBuilder { fn load_config_files(&self, config: &Config, ts: &mut Toolset) { for cf in config.config_files.values().rev() { - if self.global_only && !cf.is_global() { + if self.global_only && !config::is_global_config(cf.get_path()) { return; } ts.merge(cf.to_toolset()); diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index 585458f856..1ba636118c 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -276,7 +276,7 @@ impl Toolset { } pub fn env_with_path(&self, config: &Config) -> Result> { let mut path_env = PathEnv::from_iter(env::PATH.clone()); - for p in config.path_dirs.clone() { + for p in config.path_dirs()?.clone() { path_env.add(p); } let mut env = self.env(config)?; @@ -397,7 +397,7 @@ impl Toolset { .join(" "); warn!( "missing: {}", - truncate_str(&versions, *TERM_WIDTH - 15, "…"), + truncate_str(&versions, *TERM_WIDTH - 14, "…"), ); } } diff --git a/test/fixtures/.env b/test/fixtures/.env new file mode 100644 index 0000000000..ab720c6d77 --- /dev/null +++ b/test/fixtures/.env @@ -0,0 +1 @@ +export FOO="foo1" diff --git a/test/fixtures/.env2 b/test/fixtures/.env2 new file mode 100644 index 0000000000..02215ae27c --- /dev/null +++ b/test/fixtures/.env2 @@ -0,0 +1 @@ +export FOO2="foo2" From b76d21c57216f6dcd58f794535ceb1d403d1ac91 Mon Sep 17 00:00:00 2001 From: "mise[bot]" <123107610+mise-en-dev@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:28:47 +0000 Subject: [PATCH 2/3] Commit from GitHub Actions (test) --- src/cli/set.rs | 2 +- src/config/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/set.rs b/src/cli/set.rs index 31e147ed5b..65667a2ec7 100644 --- a/src/cli/set.rs +++ b/src/cli/set.rs @@ -50,7 +50,7 @@ impl Set { .map(|(key, (value, source))| Row { key: key.clone(), value: value.clone(), - source: display_path(&source), + source: display_path(source), }) .collect::>(); let mut table = tabled::Table::new(rows); diff --git a/src/config/mod.rs b/src/config/mod.rs index d486788e68..d06e6ce32a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -317,7 +317,7 @@ impl Config { .rev() .flat_map(|(source, cf)| cf.env_entries().into_iter().map(|e| (e, source.clone()))) .collect(); - EnvResults::resolve(&*env::PRISTINE_ENV, entries) + EnvResults::resolve(&env::PRISTINE_ENV, entries) } pub fn watch_files(&self) -> eyre::Result> { From 577749b6995aafddaad19cca757e45da69177d36 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:40:04 -0600 Subject: [PATCH 3/3] env-man --- e2e/test_go_install | 2 +- schema/mise.json | 252 ++++++++++++++++++---------------- src/config/config_file/mod.rs | 3 - src/config/env_directive.rs | 1 + src/config/settings.rs | 6 +- 5 files changed, 142 insertions(+), 122 deletions(-) diff --git a/e2e/test_go_install b/e2e/test_go_install index 495283dc56..b8f1a4e1fa 100755 --- a/e2e/test_go_install +++ b/e2e/test_go_install @@ -6,4 +6,4 @@ source "$(dirname "$0")/assert.sh" export MISE_EXPERIMENTAL=1 assert "mise x go:github.com/DarthSim/hivemind@v1.1.0 -- hivemind --version" "Hivemind version 1.1.0" -chmod -R u+w "$MISE_DATA_DIR/cache/go-"* +chmod -R u+w "$MISE_DATA_DIR/cache/go-"* || true diff --git a/schema/mise.json b/schema/mise.json index c475352c13..fde06afb41 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -54,7 +54,7 @@ "description": "mise settings", "type": "object", "additionalProperties": false, - "properties": { "$ref": "#/$defs/settings" } + "$ref": "#/$defs/settings" } }, "$defs": { @@ -221,126 +221,148 @@ ] }, "settings": { - "activate_aggressive": { - "description": "push tools to the front of PATH instead of allowing modifications of PATH after activation to take precedence", - "type": "boolean" - }, - "all_compile": { - "description": "do not use precompiled binaries for any tool", - "type": "boolean" - }, - "always_keep_download": { - "description": "should mise keep downloaded files after installation", - "type": "boolean" - }, - "always_keep_install": { - "description": "should mise keep install files after installation even if the installation fails", - "type": "boolean" - }, - "asdf_compat": { - "description": "set to true to ensure .tool-versions will be compatible with asdf", - "type": "boolean" - }, - "cargo_binstall": { - "description": "use cargo-binstall to install rust tools if available", - "type": "boolean", - "default": true - }, - "color": { - "description": "colorize output", - "type": "boolean", - "default": true - }, - "disable_default_shorthands": { - "description": "disables built-in shorthands", - "type": "boolean" - }, - "disable_tools": { - "description": "tools that should not be used", - "items": { - "description": "tool name", + "properties": { + "activate_aggressive": { + "description": "push tools to the front of PATH instead of allowing modifications of PATH after activation to take precedence", + "type": "boolean" + }, + "all_compile": { + "description": "do not use precompiled binaries for any tool", + "type": "boolean" + }, + "always_keep_download": { + "description": "should mise keep downloaded files after installation", + "type": "boolean" + }, + "always_keep_install": { + "description": "should mise keep install files after installation even if the installation fails", + "type": "boolean" + }, + "asdf_compat": { + "description": "set to true to ensure .tool-versions will be compatible with asdf", + "type": "boolean" + }, + "cargo_binstall": { + "description": "use cargo-binstall to install rust tools if available", + "type": "boolean", + "default": true + }, + "color": { + "description": "colorize output", + "type": "boolean", + "default": true + }, + "disable_default_shorthands": { + "description": "disables built-in shorthands", + "type": "boolean" + }, + "disable_tools": { + "description": "tools that should not be used", + "items": { + "description": "tool name", + "type": "string" + }, + "type": "array" + }, + "experimental": { + "description": "enable experimental features", + "type": "boolean" + }, + "jobs": { + "description": "number of tools to install in parallel, default is 4", + "type": "integer" + }, + "legacy_version_file": { + "description": "should mise parse legacy version files (e.g. .node-version)", + "type": "boolean" + }, + "legacy_version_file_disable_tools": { + "description": "tools that should not have their legacy version files parsed", + "items": { + "description": "tool name", + "type": "string" + }, + "type": "array" + }, + "node_compile": { + "description": "do not use precompiled binaries for node", + "type": "boolean" + }, + "not_found_auto_install": { + "description": "adds a shell hook to `mise activate` and shims to automatically install tools when they need to be installed", + "type": "boolean", + "default": true + }, + "paranoid": { + "description": "extra-security mode, see https://mise.jdx.dev/paranoid.html for details", + "type": "boolean" + }, + "plugin_autoupdate_last_check_duration": { + "description": "how often to check for plugin updates", "type": "string" }, - "type": "array" - }, - "experimental": { - "description": "enable experimental features", - "type": "boolean" - }, - "jobs": { - "description": "number of tools to install in parallel, default is 4", - "type": "integer" - }, - "legacy_version_file": { - "description": "should mise parse legacy version files (e.g. .node-version)", - "type": "boolean" - }, - "legacy_version_file_disable_tools": { - "description": "tools that should not have their legacy version files parsed", - "items": { - "description": "tool name", + "python_compile": { + "description": "do not use precompiled binaries for python", + "type": "boolean" + }, + "python_venv_auto_create": { + "description": "automatically create a virtualenv for python tools", + "type": "boolean" + }, + "raw": { + "description": "directly connect plugin scripts to stdin/stdout, implies --jobs=1", + "type": "boolean" + }, + "shorthands_file": { + "description": "path to file containing shorthand mappings", "type": "string" }, - "type": "array" - }, - "node_compile": { - "description": "do not use precompiled binaries for node", - "type": "boolean" - }, - "not_found_auto_install": { - "description": "adds a shell hook to `mise activate` and shims to automatically install tools when they need to be installed", - "type": "boolean", - "default": true - }, - "paranoid": { - "description": "extra-security mode, see https://mise.jdx.dev/paranoid.html for details", - "type": "boolean" - }, - "plugin_autoupdate_last_check_duration": { - "description": "how often to check for plugin updates", - "type": "string" - }, - "python_compile": { - "description": "do not use precompiled binaries for python", - "type": "boolean" - }, - "python_venv_auto_create": { - "description": "automatically create a virtualenv for python tools", - "type": "boolean" - }, - "raw": { - "description": "directly connect plugin scripts to stdin/stdout, implies --jobs=1", - "type": "boolean" - }, - "shorthands_file": { - "description": "path to file containing shorthand mappings", - "type": "string" - }, - "task_output": { - "default": "prefix", - "description": "how to display task output", - "enum": ["prefix", "interleave"], - "type": "string" - }, - "trusted_config_paths": { - "description": "config files with these prefixes will be trusted by default", - "items": { - "description": "a path to add to PATH", + "status": { + "description": "configure messages displayed when changing directories or executing tools", + "type": "object", + "additionalProperties": false, + "properties": { + "missing_tools": { + "description": "display warning when a tool is not installed", + "type": "boolean", + "default": true + }, + "show_env": { + "description": "display configured mise environment variables", + "type": "boolean" + }, + "show_tools": { + "description": "display active tools", + "type": "boolean" + } + } + }, + "task_output": { + "default": "prefix", + "description": "how to display task output", + "enum": ["prefix", "interleave"], "type": "string" }, - "type": "array" - }, - "quiet": { - "description": "suppress all non-error output", - "type": "boolean" - }, - "verbose": { - "description": "display extra output", - "type": "boolean" - }, - "yes": { - "description": "assume yes for all prompts", - "type": "boolean" + "trusted_config_paths": { + "description": "config files with these prefixes will be trusted by default", + "items": { + "description": "a path to add to PATH", + "type": "string" + }, + "type": "array" + }, + "quiet": { + "description": "suppress all non-error output", + "type": "boolean" + }, + "verbose": { + "description": "display extra output", + "type": "boolean" + }, + "yes": { + "description": "assume yes for all prompts", + "type": "boolean" + } } } } diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 5f3a6149a7..85771e7e0b 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -66,9 +66,6 @@ pub trait ConfigFile: Debug + Send + Sync { fn env_entries(&self) -> Vec { Default::default() } - fn env_paths(&self) -> Result> { - Ok(Default::default()) - } fn tasks(&self) -> Vec<&Task> { Default::default() } diff --git a/src/config/env_directive.rs b/src/config/env_directive.rs index b5d7495e0f..1baa88b540 100644 --- a/src/config/env_directive.rs +++ b/src/config/env_directive.rs @@ -75,6 +75,7 @@ impl EnvResults { for (directive, source) in input { let config_root = source.parent().unwrap(); ctx.insert("config_root", config_root); + ctx.insert("env", &env); let normalize_path = |s: String| { let s = s.strip_prefix("./").unwrap_or(&s); match s.strip_prefix("~/") { diff --git a/src/config/settings.rs b/src/config/settings.rs index f5f32785c2..0edd973053 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -48,9 +48,6 @@ pub struct Settings { pub legacy_version_file: bool, #[config(env = "MISE_LEGACY_VERSION_FILE_DISABLE_TOOLS", default = [], parse_env = list_by_comma)] pub legacy_version_file_disable_tools: BTreeSet, - /// what level of status messages to display when entering directories - #[config(nested)] - pub status: SettingsStatus, #[config(env = "MISE_NODE_COMPILE", default = false)] pub node_compile: bool, #[config(env = "MISE_NOT_FOUND_AUTO_INSTALL", default = true)] @@ -82,6 +79,9 @@ pub struct Settings { pub raw: bool, #[config(env = "MISE_SHORTHANDS_FILE")] pub shorthands_file: Option, + /// what level of status messages to display when entering directories + #[config(nested)] + pub status: SettingsStatus, #[config(env = "MISE_TASK_OUTPUT")] pub task_output: Option, #[config(env = "MISE_TRUSTED_CONFIG_PATHS", default = [], parse_env = list_by_colon)]