Skip to main content

up_rs/
env.rs

1/*!
2Builds environment variable sets for command execution.
3
4This takes in the environment of the running process, adds built-in environment variables, and uses the user's up configuration to generate the environment to pass to tasks.
5
6## Built-in Environment Variables
7
8These env vars are automatically resolved, and will override the same env var set by the user.
9
10### `UP_HARDWARE_UUID`
11
12(macOS only)
13
14The `UP_HARDWARE_UUID` maps to the UUID of the currently executing macOS device. This is particularly useful for setting per-host defaults.
15On non-macOS platforms this resolves to the empty string.
16
17*/
18use self::EnvError as E;
19use crate::utils::files;
20use color_eyre::eyre::bail;
21use color_eyre::eyre::eyre;
22use color_eyre::eyre::Result;
23use displaydoc::Display;
24use std::collections::HashMap;
25use thiserror::Error;
26use tracing::debug;
27use tracing::trace;
28
29/// Environment variable name that is automatically provided for users to refer to, particularly in
30/// the defaults `run_lib` or subcommand.
31pub const UP_HARDWARE_UUID: &str = "UP_HARDWARE_UUID";
32
33// TODO(gib): add tests for cyclical config values etc.
34/// Build a set of environment variables from the up config settings and the current command's
35/// environment..
36#[allow(clippy::implicit_hasher)]
37pub fn get_env(
38    inherit_env: Option<&Vec<String>>,
39    input_env: Option<&HashMap<String, String>>,
40) -> Result<HashMap<String, String>> {
41    let mut env: HashMap<String, String> = HashMap::new();
42    if let Some(inherited_env) = inherit_env {
43        for inherited_var in inherited_env {
44            if let Ok(value) = std::env::var(inherited_var) {
45                env.insert(inherited_var.clone(), value);
46            }
47        }
48    }
49
50    add_builtin_env_vars(&mut env)?;
51
52    let mut unresolved_env = Vec::new();
53
54    if let Some(config_env) = input_env {
55        trace!("Provided env: {config_env:#?}");
56        let mut calculated_env = HashMap::new();
57        let home_dir = files::home_dir()?;
58        for (key, val) in config_env {
59            calculated_env.insert(
60                key.clone(),
61                shellexpand::full_with_context(
62                    val,
63                    || Some(&home_dir),
64                    |k| {
65                        env.get(k).map_or_else(
66                            || {
67                                if config_env.contains_key(k) {
68                                    unresolved_env.push(key.clone());
69                                    Ok(None)
70                                } else {
71                                    Err(eyre!("Value {k} not found in inherited_env or env vars."))
72                                }
73                            },
74                            |val| Ok(Some(val)),
75                        )
76                    },
77                )
78                .map_err(|e| E::EnvLookup {
79                    var: e.var_name,
80                    source: e.cause,
81                })?
82                .into_owned(),
83            );
84        }
85        for (k, v) in calculated_env.drain() {
86            env.insert(k, v);
87        }
88    }
89
90    // Resolve unresolved env vars.
91    debug!("Unresolved env: {unresolved_env:?}");
92    while !unresolved_env.is_empty() {
93        trace!("Env so far: {env:#?}");
94        trace!("Still unresolved env: {unresolved_env:#?}");
95        let mut resolved_indices = Vec::new();
96        for (index, key) in unresolved_env.iter().enumerate() {
97            let val = env.get(key).ok_or_else(|| eyre!("How did we get here?"))?;
98            let resolved_val = shellexpand::env_with_context(val, |k| {
99                if unresolved_env.iter().any(|s| s == k) {
100                    Ok(None)
101                } else if let Some(v) = env.get(k) {
102                    resolved_indices.push(index);
103                    Ok(Some(v))
104                } else {
105                    Err(eyre!("Shouldn't be possible to hit this."))
106                }
107            })
108            .map_err(|e| E::EnvLookup {
109                var: e.var_name,
110                source: e.cause,
111            })?
112            .into_owned();
113            let val_ref = env
114                .get_mut(key)
115                .ok_or_else(|| eyre!("How did we get here?"))?;
116            *val_ref = resolved_val;
117        }
118        trace!("resolved indices: {resolved_indices:?}");
119        if resolved_indices.is_empty() {
120            bail!("Errors resolving env, do you have cycles? Unresolved env: {unresolved_env:#?}",);
121        }
122        unresolved_env = unresolved_env
123            .into_iter()
124            .enumerate()
125            .filter_map(|(index, value)| {
126                if resolved_indices.contains(&index) {
127                    None
128                } else {
129                    Some(value)
130                }
131            })
132            .collect();
133    }
134
135    debug!("Expanded config env: {env:#?}");
136    Ok(env)
137}
138
139/// Add environment variables that up generates automatically to the resolved environment.
140fn add_builtin_env_vars(env: &mut HashMap<String, String>) -> Result<()> {
141    env.insert(
142        UP_HARDWARE_UUID.to_owned(),
143        if cfg!(target_os = "macos") {
144            crate::utils::mac::get_hardware_uuid()?
145        } else {
146            String::new()
147        },
148    );
149    Ok(())
150}
151
152#[derive(Error, Debug, Display)]
153/// Errors thrown by this file.
154pub enum EnvError {
155    /// Env lookup error, please define `{var:?}` in your up.yaml:"
156    EnvLookup {
157        /// Missing env var.
158        var: String,
159        /// Source error.
160        source: color_eyre::eyre::Error,
161    },
162}