diff --git a/docs/environments/index.md b/docs/environments/index.md index 249b3bca3d..bf24850436 100644 --- a/docs/environments/index.md +++ b/docs/environments/index.md @@ -31,6 +31,18 @@ NODE_ENV development mise.toml $ mise unset NODE_ENV ``` +## Lazy eval + +Environment variables typically are resolved before tools—that way you can configure tool installation +with environment variables. However, sometimes you want to access environment variables produced by +tools. To do that, turn the value into a map with `tools = true`: + +```toml +[env] +MY_VAR = { value = "tools path: {{env.PATH}}", tools = true } +_.path = { value = ["{{env.GEM_HOME}}/bin"], tools = true } # directives may also set tools = true +``` + ## `env._` directives `env._.*` define special behavior for setting environment variables. (e.g.: reading env vars @@ -57,6 +69,15 @@ not to mise since there is not much mise can do about the way that crate works. Or set [`MISE_ENV_FILE=.env`](/configuration#mise-env-file) to automatically load dotenv files in any directory. +You can also use json or yaml files: + +```toml +[env] +_.file = '.env.json' +``` + +See [secrets](/environments/secrets) for ways to read encrypted files with `env._.file`. + ### `env._.path` `PATH` is treated specially, it needs to be defined as a string/array in `mise.path`: diff --git a/e2e/config/test_config_post_tools b/e2e/config/test_config_post_tools new file mode 100644 index 0000000000..7c6ed5db65 --- /dev/null +++ b/e2e/config/test_config_post_tools @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +cat <mise.toml +[[env]] +A_PATH = "foo: {{ env.PATH }}" +B_PATH = { value = "foo: {{ env.PATH }}", tools = true } +[[env]] +_.path = {value = "tiny-{{env.JDXCODE_TINY}}-tiny", tools = true} + +[tools] +tiny = "1.0.0" +EOF + +mise i +assert_not_contains "mise env | grep A_PATH" "tiny" +assert_contains "mise env | grep B_PATH" "tiny" + +assert_contains "mise dr path" "tiny-1.0.0-tiny" diff --git a/mise.usage.kdl b/mise.usage.kdl index c81136c6cc..379d205663 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -359,6 +359,7 @@ cmd "doctor" help="Check mise installation for possible problems" { [WARN] plugin node is not installed " cmd "path" help="Print the current PATH entries mise is providing" { + alias "paths" hide=true after_long_help r"Examples: Get the current PATH entries mise is providing diff --git a/src/cli/doctor/path.rs b/src/cli/doctor/path.rs index 143bb79650..4b92b88fb9 100644 --- a/src/cli/doctor/path.rs +++ b/src/cli/doctor/path.rs @@ -4,7 +4,7 @@ use std::env; /// Print the current PATH entries mise is providing #[derive(Debug, clap::Args)] -#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] +#[clap(alias="paths", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] pub struct Path { /// Print all entries including those not provided by mise #[clap(long, short, verbatim_doc_comment)] diff --git a/src/cli/set.rs b/src/cli/set.rs index 15a849de7c..6a2f034491 100644 --- a/src/cli/set.rs +++ b/src/cli/set.rs @@ -68,7 +68,7 @@ impl Set { if env_vars.len() == 1 && env_vars[0].value.is_none() { let key = &env_vars[0].key; match config.env_entries()?.into_iter().find_map(|ev| match ev { - EnvDirective::Val(k, v) if &k == key => Some(v), + EnvDirective::Val(k, v, _) if &k == key => Some(v), _ => None, }) { Some(value) => miseprintln!("{value}"), @@ -122,7 +122,7 @@ impl Set { .env_entries()? .into_iter() .filter_map(|ed| match ed { - EnvDirective::Val(key, value) => Some(Row { + EnvDirective::Val(key, value, _) => Some(Row { key, value, source: display_path(file), diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index 97b4649c36..338f40cc73 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -1,7 +1,3 @@ -use std::collections::{BTreeMap, HashMap}; -use std::fmt::{Debug, Formatter}; -use std::path::{Path, PathBuf}; - use eyre::{eyre, WrapErr}; use indexmap::IndexMap; use itertools::Itertools; @@ -9,14 +5,18 @@ use once_cell::sync::OnceCell; use serde::de::Visitor; use serde::{de, Deserializer}; use serde_derive::Deserialize; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::{Debug, Display, Formatter}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; use tera::Context as TeraContext; use toml_edit::{table, value, Array, DocumentMut, InlineTable, Item, Key, Value}; use versions::Versioning; use crate::cli::args::{BackendArg, ToolVersionType}; -use crate::config::config_file::toml::{deserialize_arr, deserialize_path_entry_arr}; +use crate::config::config_file::toml::deserialize_arr; use crate::config::config_file::{config_trust_root, trust, trust_check, ConfigFile, TaskConfig}; -use crate::config::env_directive::{EnvDirective, PathEntry}; +use crate::config::env_directive::{EnvDirective, EnvDirectiveOptions}; use crate::config::settings::SettingsPartial; use crate::config::{Alias, AliasMap}; use crate::file::{create_dir_all, display_path}; @@ -40,11 +40,11 @@ pub struct MiseToml { #[serde(skip)] path: PathBuf, #[serde(default, alias = "dotenv", deserialize_with = "deserialize_arr")] - env_file: Vec, + env_file: Vec, #[serde(default)] env: EnvList, #[serde(default, deserialize_with = "deserialize_arr")] - env_path: Vec, + env_path: Vec, #[serde(default)] alias: AliasMap, #[serde(skip)] @@ -79,6 +79,12 @@ pub struct MiseTomlTool { pub options: Option, } +#[derive(Debug, Clone)] +pub struct MiseTomlEnvDirective { + pub value: String, + pub options: EnvDirectiveOptions, +} + #[derive(Debug, Default, Clone)] pub struct Tasks(pub BTreeMap); @@ -278,12 +284,12 @@ impl ConfigFile for MiseToml { let path_entries = self .env_path .iter() - .map(|p| EnvDirective::Path(p.clone())) + .map(|p| EnvDirective::Path(p.clone(), Default::default())) .collect_vec(); let env_files = self .env_file .iter() - .map(|p| EnvDirective::File(p.clone())) + .map(|p| EnvDirective::File(p.clone(), Default::default())) .collect_vec(); let all = path_entries .into_iter() @@ -707,7 +713,7 @@ impl<'de> de::Deserialize<'de> for EnvList { match key.as_str() { "_" | "mise" => { struct EnvDirectivePythonVenv { - path: PathBuf, + path: String, create: bool, python: Option, uv_create_args: Option>, @@ -723,12 +729,12 @@ impl<'de> de::Deserialize<'de> for EnvList { #[derive(Deserialize)] struct EnvDirectives { - #[serde(default, deserialize_with = "deserialize_path_entry_arr")] - path: Vec, #[serde(default, deserialize_with = "deserialize_arr")] - file: Vec, + path: Vec, #[serde(default, deserialize_with = "deserialize_arr")] - source: Vec, + file: Vec, + #[serde(default, deserialize_with = "deserialize_arr")] + source: Vec, #[serde(default)] python: EnvDirectivePython, #[serde(flatten)] @@ -826,17 +832,17 @@ impl<'de> de::Deserialize<'de> for EnvList { 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 d in directives.path { + env.push(EnvDirective::Path(d.value, d.options)); } - for file in directives.file { - env.push(EnvDirective::File(file)); + for d in directives.file { + env.push(EnvDirective::File(d.value, d.options)); } - for source in directives.source { - env.push(EnvDirective::Source(source)); + for d in directives.source { + env.push(EnvDirective::Source(d.value, d.options)); } for (key, value) in directives.other { - env.push(EnvDirective::Module(key, value)); + env.push(EnvDirective::Module(key, value, Default::default())); } if let Some(venv) = directives.python.venv { env.push(EnvDirective::PythonVenv { @@ -845,6 +851,7 @@ impl<'de> de::Deserialize<'de> for EnvList { python: venv.python, uv_create_args: venv.uv_create_args, python_create_args: venv.python_create_args, + options: Default::default(), }); } } @@ -853,18 +860,33 @@ impl<'de> de::Deserialize<'de> for EnvList { Int(i64), Str(String), Bool(bool), + Map { value: Box, tools: bool }, + } + impl Display for Val { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + Val::Int(i) => write!(f, "{}", i), + Val::Str(s) => write!(f, "{}", s), + Val::Bool(b) => write!(f, "{}", b), + Val::Map { value, tools } => { + write!(f, "{}", value)?; + if *tools { + write!(f, " tools")?; + } + Ok(()) + } + } + } } impl<'de> de::Deserialize<'de> for Val { - fn deserialize( - deserializer: D, - ) -> std::result::Result + fn deserialize(deserializer: D) -> Result where - D: de::Deserializer<'de>, + D: Deserializer<'de>, { struct ValVisitor; - impl Visitor<'_> for ValVisitor { + impl<'de> Visitor<'de> for ValVisitor { type Value = Val; fn expecting( &self, @@ -878,12 +900,7 @@ impl<'de> de::Deserialize<'de> for EnvList { where E: de::Error, { - match v { - true => Err(de::Error::custom( - "env values cannot be true", - )), - false => Ok(Val::Bool(v)), - } + Ok(Val::Bool(v)) } fn visit_i64(self, v: i64) -> Result @@ -899,6 +916,46 @@ impl<'de> de::Deserialize<'de> for EnvList { { Ok(Val::Str(v.to_string())) } + + fn visit_map( + self, + mut map: A, + ) -> Result + where + A: de::MapAccess<'de>, + { + let mut value: Option = None; + let mut tools = None; + while let Some((key, val)) = + map.next_entry::()? + { + match key.as_str() { + "value" => { + value = Some(val); + } + "tools" => { + tools = Some(val); + } + _ => { + return Err(de::Error::unknown_field( + &key, + &["value", "tools"], + )); + } + } + } + let value = value + .ok_or_else(|| de::Error::missing_field("value"))?; + let tools = if let Some(Val::Bool(tools)) = tools { + tools + } else { + false + }; + Ok(Val::Map { + value: Box::new(value), + tools, + }) + } } deserializer.deserialize_any(ValVisitor) @@ -908,12 +965,27 @@ impl<'de> de::Deserialize<'de> for EnvList { let value = map.next_value::()?; match value { Val::Int(i) => { - env.push(EnvDirective::Val(key, i.to_string())); + env.push(EnvDirective::Val( + key, + i.to_string(), + Default::default(), + )); } Val::Str(s) => { - env.push(EnvDirective::Val(key, s)); + env.push(EnvDirective::Val(key, s, Default::default())); + } + Val::Bool(true) => env.push(EnvDirective::Val( + key, + "true".into(), + Default::default(), + )), + Val::Bool(false) => { + env.push(EnvDirective::Rm(key, Default::default())) + } + Val::Map { value, tools } => { + let opts = EnvDirectiveOptions { tools }; + env.push(EnvDirective::Val(key, value.to_string(), opts)); } - Val::Bool(_b) => env.push(EnvDirective::Rm(key)), } } } @@ -1028,6 +1100,71 @@ impl<'de> de::Deserialize<'de> for MiseTomlToolList { } } +impl FromStr for MiseTomlEnvDirective { + type Err = eyre::Report; + + fn from_str(s: &str) -> eyre::Result { + Ok(MiseTomlEnvDirective { + value: s.into(), + options: Default::default(), + }) + } +} + +impl<'de> de::Deserialize<'de> for MiseTomlEnvDirective { + fn deserialize(deserializer: D) -> std::result::Result + where + D: de::Deserializer<'de>, + { + struct MiseTomlEnvDirectiveVisitor; + + impl<'de> Visitor<'de> for MiseTomlEnvDirectiveVisitor { + type Value = MiseTomlEnvDirective; + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("env directive") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(MiseTomlEnvDirective { + value: v.into(), + options: Default::default(), + }) + } + + fn visit_map(self, mut map: M) -> std::result::Result + where + M: de::MapAccess<'de>, + { + let mut options: EnvDirectiveOptions = Default::default(); + let mut value = None; + while let Some((k, v)) = map.next_entry::()? { + match k.as_str() { + "value" => { + value = Some(v.as_str().unwrap().to_string()); + } + "tools" => { + options.tools = v.as_bool().unwrap(); + } + _ => { + return Err(de::Error::custom("invalid key")); + } + } + } + if let Some(value) = value { + Ok(MiseTomlEnvDirective { value, options }) + } else { + Err(de::Error::custom("missing value")) + } + } + } + + deserializer.deserialize_any(MiseTomlEnvDirectiveVisitor) + } +} + impl<'de> de::Deserialize<'de> for MiseTomlTool { fn deserialize(deserializer: D) -> std::result::Result where 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 d4b7931c20..8a07b4aea5 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 @@ -9,14 +9,23 @@ MiseToml(~/cwd/.test.mise.toml): ToolRequestSet: { Val( "foo", "bar", + EnvDirectiveOptions { + tools: false, + }, ), Val( "foo2", "qux\\nquux", + EnvDirectiveOptions { + tools: false, + }, ), Val( "foo3", "qux\nquux", + EnvDirectiveOptions { + tools: false, + }, ), ], } 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 21c808de6a..444b8461d3 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 @@ -8,6 +8,9 @@ MiseToml(~/fixtures/.mise.toml): ToolRequestSet: terraform@1.0.0 node@18 node@pr Val( "NODE_ENV", "production", + EnvDirectiveOptions { + tools: false, + }, ), ], alias: { 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 ca870f53b2..7d2f45c7c7 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 @@ -7,5 +7,8 @@ snapshot_kind: text Val( "NODE_ENV", "production", + EnvDirectiveOptions { + tools: false, + }, ), ] diff --git a/src/config/config_file/toml.rs b/src/config/config_file/toml.rs index 2dcc07aa18..69f0cc5856 100644 --- a/src/config/config_file/toml.rs +++ b/src/config/config_file/toml.rs @@ -1,9 +1,9 @@ use std::collections::BTreeMap; -use std::fmt::{Debug, Formatter}; +use std::fmt::Formatter; use std::str::FromStr; use either::Either; -use serde::de; +use serde::{de, Deserialize}; use crate::task::{EitherIntOrBool, EitherStringOrIntOrBool}; @@ -88,14 +88,14 @@ impl<'a> TomlParser<'a> { pub fn deserialize_arr<'de, D, T>(deserializer: D) -> eyre::Result, D::Error> where D: de::Deserializer<'de>, - T: FromStr, + T: FromStr + Deserialize<'de>, ::Err: std::fmt::Display, { struct ArrVisitor(std::marker::PhantomData); impl<'de, T> de::Visitor<'de> for ArrVisitor where - T: FromStr, + T: FromStr + Deserialize<'de>, ::Err: std::fmt::Display, { type Value = Vec; @@ -121,49 +121,16 @@ where } Ok(v) } - } - - deserializer.deserialize_any(ArrVisitor(std::marker::PhantomData)) -} - -pub fn deserialize_path_entry_arr<'de, D, T>(deserializer: D) -> eyre::Result, D::Error> -where - D: de::Deserializer<'de>, - T: FromStr + Debug + serde::Deserialize<'de>, - ::Err: std::fmt::Display, -{ - struct PathEntryArrVisitor(std::marker::PhantomData); - - impl<'de, T> de::Visitor<'de> for PathEntryArrVisitor - where - T: FromStr + Debug + serde::Deserialize<'de>, - ::Err: std::fmt::Display, - { - type Value = Vec; - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("path entry or array of path entries") - } - fn visit_str(self, v: &str) -> std::result::Result + fn visit_map(self, map: M) -> std::result::Result where - E: de::Error, + M: de::MapAccess<'de>, { - 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(entry) = seq.next_element::()? { - trace!("visit_seq: entry: {:?}", entry); - v.push(entry); - } - Ok(v) + Ok(vec![Deserialize::deserialize( + de::value::MapAccessDeserializer::new(map), + )?]) } } - deserializer.deserialize_any(PathEntryArrVisitor(std::marker::PhantomData)) + deserializer.deserialize_any(ArrVisitor(std::marker::PhantomData)) } diff --git a/src/config/env_directive/file.rs b/src/config/env_directive/file.rs index cb2542b424..3bc715d5dd 100644 --- a/src/config/env_directive/file.rs +++ b/src/config/env_directive/file.rs @@ -18,19 +18,21 @@ struct Env { } impl EnvResults { + #[allow(clippy::too_many_arguments)] pub fn file( ctx: &mut tera::Context, + tera: &mut tera::Tera, env: &mut IndexMap)>, r: &mut EnvResults, normalize_path: fn(&Path, PathBuf) -> PathBuf, source: &Path, config_root: &Path, - input: PathBuf, + input: String, ) -> Result<()> { - let s = r.parse_template(ctx, source, input.to_string_lossy().as_ref())?; + let s = r.parse_template(ctx, tera, source, &input)?; for p in xx::file::glob(normalize_path(config_root, s.into())).unwrap_or_default() { r.env_files.push(p.clone()); - let parse_template = |s: String| r.parse_template(ctx, source, &s); + let parse_template = |s: String| r.parse_template(ctx, tera, source, &s); let ext = p .extension() .map(|e| e.to_string_lossy().to_string()) @@ -51,7 +53,7 @@ impl EnvResults { fn json(p: &Path, parse_template: PT) -> Result where - PT: Fn(String) -> Result, + PT: FnMut(String) -> Result, { let errfn = || eyre!("failed to parse json file: {}", display_path(p)); if let Ok(raw) = file::read_to_string(p) { @@ -81,7 +83,7 @@ impl EnvResults { fn yaml(p: &Path, parse_template: PT) -> Result where - PT: Fn(String) -> Result, + PT: FnMut(String) -> Result, { let errfn = || eyre!("failed to parse yaml file: {}", display_path(p)); if let Ok(raw) = file::read_to_string(p) { diff --git a/src/config/env_directive/mod.rs b/src/config/env_directive/mod.rs index 6926bb5c6c..48de446075 100644 --- a/src/config/env_directive/mod.rs +++ b/src/config/env_directive/mod.rs @@ -1,9 +1,9 @@ +use crate::env; use std::collections::{BTreeSet, HashMap}; -use std::env; use std::fmt::{Debug, Display, Formatter}; use std::path::{Path, PathBuf}; -use std::str::FromStr; +use crate::cmd::cmd; use crate::config::config_file::{config_root, trust_check}; use crate::dirs; use crate::env_diff::EnvMap; @@ -11,7 +11,6 @@ use crate::file::display_path; use crate::tera::get_tera; use eyre::{eyre, Context}; use indexmap::IndexMap; -use serde::{Deserialize, Deserializer}; mod file; mod module; @@ -19,84 +18,51 @@ mod path; mod source; mod venv; -#[derive(Debug, Clone)] -pub enum PathEntry { - Normal(PathBuf), - Lazy(PathBuf), -} - -impl From<&str> for PathEntry { - fn from(s: &str) -> Self { - let pb = PathBuf::from(s); - Self::Normal(pb) - } -} - -impl FromStr for PathEntry { - type Err = eyre::Error; - - fn from_str(s: &str) -> eyre::Result { - let pb = PathBuf::from_str(s)?; - Ok(Self::Normal(pb)) - } -} - -impl AsRef for PathEntry { - #[inline] - fn as_ref(&self) -> &Path { - match self { - PathEntry::Normal(pb) => pb.as_ref(), - PathEntry::Lazy(pb) => pb.as_ref(), - } - } -} - -impl<'de> Deserialize<'de> for PathEntry { - fn deserialize>(deserializer: D) -> std::result::Result { - #[derive(Debug, Deserialize)] - struct MapPathEntry { - value: PathBuf, - } - - #[derive(Debug, Deserialize)] - #[serde(untagged)] - enum Helper { - Normal(PathBuf), - Lazy(MapPathEntry), - } - - Ok(match Helper::deserialize(deserializer)? { - Helper::Normal(value) => Self::Normal(value), - Helper::Lazy(this) => Self::Lazy(this.value), - }) - } +#[derive(Debug, Clone, Default)] +pub struct EnvDirectiveOptions { + pub(crate) tools: bool, } #[derive(Debug, Clone)] pub enum EnvDirective { /// simple key/value pair - Val(String, String), + Val(String, String, EnvDirectiveOptions), /// remove a key - Rm(String), + Rm(String, EnvDirectiveOptions), /// dotenv file - File(PathBuf), + File(String, EnvDirectiveOptions), /// add a path to the PATH - Path(PathEntry), + Path(String, EnvDirectiveOptions), /// run a bash script and apply the resulting env diff - Source(PathBuf), + Source(String, EnvDirectiveOptions), PythonVenv { - path: PathBuf, + path: String, create: bool, python: Option, uv_create_args: Option>, python_create_args: Option>, + options: EnvDirectiveOptions, }, - Module(String, toml::Value), + Module(String, toml::Value, EnvDirectiveOptions), +} + +impl EnvDirective { + pub fn options(&self) -> &EnvDirectiveOptions { + match self { + EnvDirective::Val(_, _, opts) + | EnvDirective::Rm(_, opts) + | EnvDirective::File(_, opts) + | EnvDirective::Path(_, opts) + | EnvDirective::Source(_, opts) + | EnvDirective::PythonVenv { options: opts, .. } + | EnvDirective::Module(_, _, opts) => opts, + } + } } impl From<(String, String)> for EnvDirective { fn from((k, v): (String, String)) -> Self { - Self::Val(k, v) + Self::Val(k, v, Default::default()) } } @@ -109,18 +75,19 @@ impl From<(String, i64)> for EnvDirective { 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)), - EnvDirective::Module(name, _) => write!(f, "module {}", name), + 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)), + EnvDirective::Module(name, _, _) => write!(f, "module {}", name), EnvDirective::PythonVenv { path, create, python, uv_create_args, python_create_args, + .. } => { write!(f, "python venv path={}", display_path(path))?; if *create { @@ -155,6 +122,7 @@ impl EnvResults { mut ctx: tera::Context, initial: &EnvMap, input: Vec<(EnvDirective, PathBuf)>, + tools: bool, ) -> eyre::Result { // trace!("resolve: input: {:#?}", &input); let mut env = initial @@ -176,8 +144,33 @@ impl EnvResults { _ => p.to_path_buf(), } }; - let mut paths: Vec<(PathEntry, PathBuf)> = Vec::new(); + let mut paths: Vec<(PathBuf, PathBuf)> = Vec::new(); for (directive, source) in input.clone() { + if directive.options().tools != tools { + continue; + } + let mut tera = get_tera(source.parent()); + tera.register_function("exec", { + let source = source.clone(); + let env = env.clone(); + move |args: &HashMap| -> tera::Result { + match args.get("command") { + Some(tera::Value::String(command)) => { + let env = env::PRISTINE_ENV + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .chain(env.iter().map(|(k, (v, _))| (k.to_string(), v.to_string()))) + .collect::(); + let result = cmd("bash", ["-c", command]) + .full_env(&env) + .dir(config_root(&source)) + .read()?; + Ok(tera::Value::String(result)) + } + _ => Err("exec command must be a string".into()), + } + } + }); // trace!( // "resolve: directive: {:?}, source: {:?}", // &directive, @@ -193,22 +186,23 @@ impl EnvResults { ctx.insert("env", &env_vars); // trace!("resolve: ctx.get('env'): {:#?}", &ctx.get("env")); match directive { - EnvDirective::Val(k, v) => { - let v = r.parse_template(&ctx, &source, &v)?; + EnvDirective::Val(k, v, _opts) => { + let v = r.parse_template(&ctx, &mut tera, &source, &v)?; r.env_remove.remove(&k); // trace!("resolve: inserting {:?}={:?} from {:?}", &k, &v, &source); env.insert(k, (v, Some(source.clone()))); } - EnvDirective::Rm(k) => { + EnvDirective::Rm(k, _opts) => { env.shift_remove(&k); r.env_remove.insert(k); } - EnvDirective::Path(input_str) => { - Self::path(&mut ctx, &mut r, &mut paths, source, input_str)?; + EnvDirective::Path(input_str, _opts) => { + Self::path(&mut ctx, &mut tera, &mut r, &mut paths, source, input_str)?; } - EnvDirective::File(input) => { + EnvDirective::File(input, _opts) => { Self::file( &mut ctx, + &mut tera, &mut env, &mut r, normalize_path, @@ -217,9 +211,10 @@ impl EnvResults { input, )?; } - EnvDirective::Source(input) => { + EnvDirective::Source(input, _opts) => { Self::source( &mut ctx, + &mut tera, &mut env, &mut r, normalize_path, @@ -235,9 +230,11 @@ impl EnvResults { python, uv_create_args, python_create_args, + options: _opts, } => { Self::venv( &mut ctx, + &mut tera, &mut env, &mut r, normalize_path, @@ -251,7 +248,7 @@ impl EnvResults { python_create_args, )?; } - EnvDirective::Module(name, value) => { + EnvDirective::Module(name, value, _opts) => { Self::module(&mut r, source, name, &value)?; } }; @@ -268,21 +265,14 @@ impl EnvResults { } // trace!("resolve: paths: {:#?}", &paths); // trace!("resolve: ctx.env: {:#?}", &ctx.get("env")); - for (entry, source) in paths { + for (p, source) in paths { // trace!("resolve: entry: {:?}, source: {}", &entry, display_path(source)); let config_root = source .parent() .map(Path::to_path_buf) .or_else(|| dirs::CWD.clone()) .unwrap_or_default(); - let s = match entry { - PathEntry::Normal(pb) => pb.to_string_lossy().to_string(), - PathEntry::Lazy(pb) => { - // trace!("resolve: s: {:?}", &s); - r.parse_template(&ctx, &source, pb.to_string_lossy().as_ref())? - } - }; - env::split_paths(&s) + env::split_paths(&p) .map(|s| normalize_path(&config_root, s)) .for_each(|p| r.env_paths.push(p.clone())); } @@ -292,6 +282,7 @@ impl EnvResults { fn parse_template( &self, ctx: &tera::Context, + tera: &mut tera::Tera, path: &Path, input: &str, ) -> eyre::Result { @@ -299,8 +290,7 @@ impl EnvResults { return Ok(input.to_string()); } trust_check(path)?; - let dir = path.parent(); - let output = get_tera(dir) + let output = tera .render_str(input, ctx) .wrap_err_with(|| eyre!("failed to parse template: '{input}'"))?; Ok(output) diff --git a/src/config/env_directive/path.rs b/src/config/env_directive/path.rs index 9d5807430e..13d0a74441 100644 --- a/src/config/env_directive/path.rs +++ b/src/config/env_directive/path.rs @@ -1,36 +1,25 @@ -use crate::config::env_directive::{EnvResults, PathEntry}; +use crate::config::env_directive::EnvResults; use crate::result; use std::path::PathBuf; impl EnvResults { pub fn path( ctx: &mut tera::Context, + tera: &mut tera::Tera, r: &mut EnvResults, - paths: &mut Vec<(PathEntry, PathBuf)>, + paths: &mut Vec<(PathBuf, PathBuf)>, source: PathBuf, - input_str: PathEntry, + input: String, ) -> result::Result<()> { // trace!("resolve: input_str: {:#?}", input_str); - match input_str { - PathEntry::Normal(input) => { - // trace!( - // "resolve: normal: input: {:?}, input.to_string(): {:?}", - // &input, - // input.to_string_lossy().as_ref() - // ); - let s = r.parse_template(ctx, &source, input.to_string_lossy().as_ref())?; - // trace!("resolve: s: {:?}", &s); - paths.push((PathEntry::Normal(s.into()), source)); - } - PathEntry::Lazy(input) => { - // trace!( - // "resolve: lazy: input: {:?}, input.to_string(): {:?}", - // &input, - // input.to_string_lossy().as_ref() - // ); - paths.push((PathEntry::Lazy(input), source)); - } - } + // trace!( + // "resolve: normal: input: {:?}, input.to_string(): {:?}", + // &input, + // input.to_string_lossy().as_ref() + // ); + let s = r.parse_template(ctx, tera, &source, &input)?; + // trace!("resolve: s: {:?}", &s); + paths.push((s.into(), source)); Ok(()) } } @@ -56,22 +45,26 @@ mod tests { &env, vec![ ( - EnvDirective::Path("/path/1".into()), + EnvDirective::Path("/path/1".into(), Default::default()), PathBuf::from("/config"), ), ( - EnvDirective::Path("/path/2".into()), + EnvDirective::Path("/path/2".into(), Default::default()), PathBuf::from("/config"), ), ( - EnvDirective::Path("~/foo/{{ env.A }}".into()), + EnvDirective::Path("~/foo/{{ env.A }}".into(), Default::default()), Default::default(), ), ( - EnvDirective::Path("./rel/{{ env.A }}:./rel2/{{env.B}}".into()), + EnvDirective::Path( + "./rel/{{ env.A }}:./rel2/{{env.B}}".into(), + Default::default(), + ), Default::default(), ), ], + false, ) .unwrap(); assert_debug_snapshot!( diff --git a/src/config/env_directive/source.rs b/src/config/env_directive/source.rs index ec52210a38..d846a12406 100644 --- a/src/config/env_directive/source.rs +++ b/src/config/env_directive/source.rs @@ -7,15 +7,16 @@ impl EnvResults { #[allow(clippy::too_many_arguments)] pub fn source( ctx: &mut tera::Context, + tera: &mut tera::Tera, env: &mut IndexMap)>, r: &mut EnvResults, normalize_path: fn(&Path, PathBuf) -> PathBuf, source: &Path, config_root: &Path, env_vars: &EnvMap, - input: PathBuf, + input: String, ) { - if let Ok(s) = r.parse_template(ctx, source, input.to_string_lossy().as_ref()) { + if let Ok(s) = r.parse_template(ctx, tera, source, &input) { for p in xx::file::glob(normalize_path(config_root, s.into())).unwrap_or_default() { r.env_scripts.push(p.clone()); let env_diff = EnvDiff::from_bash_script(&p, config_root, env_vars.clone()) diff --git a/src/config/env_directive/venv.rs b/src/config/env_directive/venv.rs index f2d54c7874..2d1a26b5fe 100644 --- a/src/config/env_directive/venv.rs +++ b/src/config/env_directive/venv.rs @@ -16,13 +16,14 @@ impl EnvResults { #[allow(clippy::too_many_arguments)] pub fn venv( ctx: &mut tera::Context, + tera: &mut tera::Tera, env: &mut IndexMap)>, r: &mut EnvResults, normalize_path: fn(&Path, PathBuf) -> PathBuf, source: &Path, config_root: &Path, env_vars: EnvMap, - path: PathBuf, + path: String, create: bool, python: Option, uv_create_args: Option>, @@ -30,7 +31,7 @@ impl EnvResults { ) -> Result<()> { trace!("python venv: {} create={create}", display_path(&path)); trust_check(source)?; - let venv = r.parse_template(ctx, source, path.to_string_lossy().as_ref())?; + let venv = r.parse_template(ctx, tera, source, &path)?; let venv = normalize_path(config_root, venv.into()); if !venv.exists() && create { // TODO: the toolset stuff doesn't feel like it's in the right place here @@ -162,25 +163,28 @@ mod tests { vec![ ( EnvDirective::PythonVenv { - path: PathBuf::from("/"), + path: "/".into(), create: false, python: None, uv_create_args: None, python_create_args: None, + options: Default::default(), }, Default::default(), ), ( EnvDirective::PythonVenv { - path: PathBuf::from("./"), + path: "./".into(), create: false, python: None, uv_create_args: None, python_create_args: None, + options: Default::default(), }, Default::default(), ), ], + false, ) .unwrap(); // expect order to be reversed as it processes directives from global to dir specific diff --git a/src/config/mod.rs b/src/config/mod.rs index bb15ec53be..ef71cbc50e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -582,7 +582,8 @@ impl Config { .flatten() .collect(); // trace!("load_env: entries: {:#?}", entries); - let env_results = EnvResults::resolve(self.tera_ctx.clone(), &env::PRISTINE_ENV, entries)?; + let env_results = + EnvResults::resolve(self.tera_ctx.clone(), &env::PRISTINE_ENV, entries, false)?; time!("load_env done"); if log::log_enabled!(log::Level::Trace) { trace!("{env_results:#?}"); @@ -1080,7 +1081,7 @@ fn load_vars(ctx: tera::Context, config_files: &ConfigMap) -> Result .into_iter() .flatten() .collect(); - let vars_results = EnvResults::resolve(ctx, &env::PRISTINE_ENV, entries)?; + let vars_results = EnvResults::resolve(ctx, &env::PRISTINE_ENV, entries, false)?; time!("load_vars done"); if log::log_enabled!(log::Level::Trace) { trace!("{vars_results:#?}"); diff --git a/src/sops.rs b/src/sops.rs index fe78140b3e..b8a3ca7943 100644 --- a/src/sops.rs +++ b/src/sops.rs @@ -9,9 +9,9 @@ use rops::file::RopsFile; use std::env; use std::sync::{Mutex, OnceLock}; -pub fn decrypt(input: &str, parse_template: PT, format: &str) -> result::Result +pub fn decrypt(input: &str, mut parse_template: PT, format: &str) -> result::Result where - PT: Fn(String) -> result::Result, + PT: FnMut(String) -> result::Result, F: rops::file::format::FileFormat, { static AGE_KEY: OnceLock> = OnceLock::new(); diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index 1f82e82b5b..837588de3f 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -6,6 +6,7 @@ use std::{panic, thread}; use crate::backend::Backend; use crate::cli::args::BackendArg; +use crate::config::env_directive::EnvResults; use crate::config::settings::{SettingsStatusMissingTools, SETTINGS}; use crate::config::Config; use crate::env::{PATH_KEY, TERM_WIDTH}; @@ -454,6 +455,14 @@ impl Toolset { path_env.add(p); } env.insert(PATH_KEY.to_string(), path_env.to_string()); + let mut ctx = config.tera_ctx.clone(); + ctx.insert("env", &env); + env.extend( + self.load_post_env(ctx, &env)? + .env + .into_iter() + .map(|(k, v)| (k, v.0)), + ); Ok(env) } pub fn env_from_tools(&self, config: &Config) -> Vec<(String, String, String)> { @@ -533,6 +542,15 @@ impl Toolset { for p in self.list_paths() { paths.insert(p); } + let config = Config::get(); + let mut env = self.env(&config)?; + env.insert( + PATH_KEY.to_string(), + env::join_paths(paths.iter())?.to_string_lossy().to_string(), + ); + let mut ctx = config.tera_ctx.clone(); + ctx.insert("env", &env); + paths.extend(self.load_post_env(ctx, &env)?.env_paths); Ok(paths.into_iter().collect()) } pub fn which(&self, bin_name: &str) -> Option<(Arc, ToolVersion)> { @@ -633,6 +651,30 @@ impl Toolset { fn is_disabled(&self, ba: &BackendArg) -> bool { !ba.is_os_supported() || SETTINGS.disable_tools().contains(&ba.short) } + + fn load_post_env(&self, ctx: tera::Context, env: &EnvMap) -> Result { + let config = Config::get(); + let entries = config + .config_files + .iter() + .rev() + .map(|(source, cf)| { + cf.env_entries() + .map(|ee| ee.into_iter().map(|e| (e, source.clone()))) + }) + .collect::>>()? + .into_iter() + .flatten() + .collect(); + // trace!("load_env: entries: {:#?}", entries); + let env_results = EnvResults::resolve(ctx, env, entries, true)?; + if log::log_enabled!(log::Level::Trace) { + trace!("{env_results:#?}"); + } else { + debug!("{env_results:?}"); + } + Ok(env_results) + } } fn show_python_install_hint(versions: &[ToolRequest]) {