feat: lazy env eval (#3598)

Fixes https://github.com/jdx/mise/issues/1912
This commit is contained in:
jdx 2024-12-16 10:00:05 -06:00 committed by GitHub
parent 234975c43a
commit ca33515f3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 407 additions and 215 deletions

View File

@ -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`:

View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
cat <<EOF >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"

View File

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

View File

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

View File

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

View File

@ -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<PathBuf>,
env_file: Vec<String>,
#[serde(default)]
env: EnvList,
#[serde(default, deserialize_with = "deserialize_arr")]
env_path: Vec<PathEntry>,
env_path: Vec<String>,
#[serde(default)]
alias: AliasMap,
#[serde(skip)]
@ -79,6 +79,12 @@ pub struct MiseTomlTool {
pub options: Option<ToolVersionOptions>,
}
#[derive(Debug, Clone)]
pub struct MiseTomlEnvDirective {
pub value: String,
pub options: EnvDirectiveOptions,
}
#[derive(Debug, Default, Clone)]
pub struct Tasks(pub BTreeMap<String, Task>);
@ -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<String>,
uv_create_args: Option<Vec<String>>,
@ -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<PathEntry>,
#[serde(default, deserialize_with = "deserialize_arr")]
file: Vec<PathBuf>,
path: Vec<MiseTomlEnvDirective>,
#[serde(default, deserialize_with = "deserialize_arr")]
source: Vec<PathBuf>,
file: Vec<MiseTomlEnvDirective>,
#[serde(default, deserialize_with = "deserialize_arr")]
source: Vec<MiseTomlEnvDirective>,
#[serde(default)]
python: EnvDirectivePython,
#[serde(flatten)]
@ -826,17 +832,17 @@ impl<'de> de::Deserialize<'de> for EnvList {
let directives = map.next_value::<EnvDirectives>()?;
// 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<Val>, 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<D>(
deserializer: D,
) -> std::result::Result<Self, D::Error>
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: i64) -> Result<Self::Value, E>
@ -899,6 +916,46 @@ impl<'de> de::Deserialize<'de> for EnvList {
{
Ok(Val::Str(v.to_string()))
}
fn visit_map<A>(
self,
mut map: A,
) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut value: Option<Val> = None;
let mut tools = None;
while let Some((key, val)) =
map.next_entry::<String, Val>()?
{
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::<Val>()?;
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<Self> {
Ok(MiseTomlEnvDirective {
value: s.into(),
options: Default::default(),
})
}
}
impl<'de> de::Deserialize<'de> for MiseTomlEnvDirective {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(MiseTomlEnvDirective {
value: v.into(),
options: Default::default(),
})
}
fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut options: EnvDirectiveOptions = Default::default();
let mut value = None;
while let Some((k, v)) = map.next_entry::<String, toml::Value>()? {
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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where

View File

@ -9,14 +9,23 @@ MiseToml(~/cwd/.test.mise.toml): ToolRequestSet: <empty> {
Val(
"foo",
"bar",
EnvDirectiveOptions {
tools: false,
},
),
Val(
"foo2",
"qux\\nquux",
EnvDirectiveOptions {
tools: false,
},
),
Val(
"foo3",
"qux\nquux",
EnvDirectiveOptions {
tools: false,
},
),
],
}

View File

@ -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: {

View File

@ -7,5 +7,8 @@ snapshot_kind: text
Val(
"NODE_ENV",
"production",
EnvDirectiveOptions {
tools: false,
},
),
]

View File

@ -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<Vec<T>, D::Error>
where
D: de::Deserializer<'de>,
T: FromStr,
T: FromStr + Deserialize<'de>,
<T as FromStr>::Err: std::fmt::Display,
{
struct ArrVisitor<T>(std::marker::PhantomData<T>);
impl<'de, T> de::Visitor<'de> for ArrVisitor<T>
where
T: FromStr,
T: FromStr + Deserialize<'de>,
<T as FromStr>::Err: std::fmt::Display,
{
type Value = Vec<T>;
@ -121,49 +121,16 @@ where
}
Ok(v)
}
fn visit_map<M>(self, map: M) -> std::result::Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
Ok(vec![Deserialize::deserialize(
de::value::MapAccessDeserializer::new(map),
)?])
}
}
deserializer.deserialize_any(ArrVisitor(std::marker::PhantomData))
}
pub fn deserialize_path_entry_arr<'de, D, T>(deserializer: D) -> eyre::Result<Vec<T>, D::Error>
where
D: de::Deserializer<'de>,
T: FromStr + Debug + serde::Deserialize<'de>,
<T as FromStr>::Err: std::fmt::Display,
{
struct PathEntryArrVisitor<T>(std::marker::PhantomData<T>);
impl<'de, T> de::Visitor<'de> for PathEntryArrVisitor<T>
where
T: FromStr + Debug + serde::Deserialize<'de>,
<T as FromStr>::Err: std::fmt::Display,
{
type Value = Vec<T>;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("path entry or array of path entries")
}
fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
let v = v.parse().map_err(de::Error::custom)?;
Ok(vec![v])
}
fn visit_seq<S>(self, mut seq: S) -> std::result::Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
let mut v = vec![];
while let Some(entry) = seq.next_element::<T>()? {
trace!("visit_seq: entry: {:?}", entry);
v.push(entry);
}
Ok(v)
}
}
deserializer.deserialize_any(PathEntryArrVisitor(std::marker::PhantomData))
}

View File

@ -18,19 +18,21 @@ struct Env<V> {
}
impl EnvResults {
#[allow(clippy::too_many_arguments)]
pub fn file(
ctx: &mut tera::Context,
tera: &mut tera::Tera,
env: &mut IndexMap<String, (String, Option<PathBuf>)>,
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<PT>(p: &Path, parse_template: PT) -> Result<EnvMap>
where
PT: Fn(String) -> Result<String>,
PT: FnMut(String) -> Result<String>,
{
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<PT>(p: &Path, parse_template: PT) -> Result<EnvMap>
where
PT: Fn(String) -> Result<String>,
PT: FnMut(String) -> Result<String>,
{
let errfn = || eyre!("failed to parse yaml file: {}", display_path(p));
if let Ok(raw) = file::read_to_string(p) {

View File

@ -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<Self> {
let pb = PathBuf::from_str(s)?;
Ok(Self::Normal(pb))
}
}
impl AsRef<Path> 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<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[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<String>,
uv_create_args: Option<Vec<String>>,
python_create_args: Option<Vec<String>>,
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<Self> {
// 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<String, tera::Value>| -> tera::Result<tera::Value> {
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::<EnvMap>();
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<String> {
@ -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)

View File

@ -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!(

View File

@ -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<String, (String, Option<PathBuf>)>,
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())

View File

@ -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<String, (String, Option<PathBuf>)>,
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<String>,
uv_create_args: Option<Vec<String>>,
@ -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

View File

@ -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<EnvResults>
.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:#?}");

View File

@ -9,9 +9,9 @@ use rops::file::RopsFile;
use std::env;
use std::sync::{Mutex, OnceLock};
pub fn decrypt<PT, F>(input: &str, parse_template: PT, format: &str) -> result::Result<String>
pub fn decrypt<PT, F>(input: &str, mut parse_template: PT, format: &str) -> result::Result<String>
where
PT: Fn(String) -> result::Result<String>,
PT: FnMut(String) -> result::Result<String>,
F: rops::file::format::FileFormat,
{
static AGE_KEY: OnceLock<Option<String>> = OnceLock::new();

View File

@ -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<dyn Backend>, 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<EnvResults> {
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::<Result<Vec<_>>>()?
.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]) {