Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bb075cf

Browse files
ssddOnToptusharmathautofix-ci[bot]
authored
feat: support environment variables across directory hierarchy (tailcallhq#803)
Co-authored-by: Tushar Mathur <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 7e2bbaa commit bb075cf

1 file changed

Lines changed: 123 additions & 3 deletions

File tree

crates/forge_infra/src/env.rs

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
use std::path::PathBuf;
1+
use std::path::{Path, PathBuf};
2+
use std::sync::RwLock;
23

34
use forge_domain::{Environment, Provider, RetryConfig};
45

56
pub struct ForgeEnvironmentService {
67
restricted: bool,
8+
is_env_loaded: RwLock<bool>,
79
}
810

911
type ProviderSearch = (&'static str, Box<dyn FnOnce(&str) -> Provider>);
@@ -15,7 +17,7 @@ impl ForgeEnvironmentService {
1517
/// * `unrestricted` - If true, use unrestricted shell mode (sh/bash) If
1618
/// false, use restricted shell mode (rbash)
1719
pub fn new(restricted: bool) -> Self {
18-
Self { restricted }
20+
Self { restricted, is_env_loaded: Default::default() }
1921
}
2022

2123
/// Get path to appropriate shell based on platform and mode
@@ -109,8 +111,12 @@ impl ForgeEnvironmentService {
109111
}
110112

111113
fn get(&self) -> Environment {
112-
dotenv::dotenv().ok();
113114
let cwd = std::env::current_dir().unwrap_or(PathBuf::from("."));
115+
if !self.is_env_loaded.read().map(|v| *v).unwrap_or_default() {
116+
*self.is_env_loaded.write().unwrap() = true;
117+
Self::dot_env(&cwd);
118+
}
119+
114120
let provider = self.resolve_provider();
115121
let retry_config = self.resolve_retry_config();
116122

@@ -127,10 +133,124 @@ impl ForgeEnvironmentService {
127133
retry_config,
128134
}
129135
}
136+
137+
/// Load all `.env` files with priority to lower (closer) files.
138+
fn dot_env(cwd: &Path) -> Option<()> {
139+
let mut paths = vec![];
140+
let mut current = PathBuf::new();
141+
142+
for component in cwd.components() {
143+
current.push(component);
144+
paths.push(current.clone());
145+
}
146+
147+
paths.reverse();
148+
149+
for path in paths {
150+
let env_file = path.join(".env");
151+
if env_file.is_file() {
152+
dotenv::from_path(&env_file).ok();
153+
}
154+
}
155+
156+
Some(())
157+
}
130158
}
131159

132160
impl forge_domain::EnvironmentService for ForgeEnvironmentService {
133161
fn get_environment(&self) -> Environment {
134162
self.get()
135163
}
136164
}
165+
166+
#[cfg(test)]
167+
mod tests {
168+
use std::path::PathBuf;
169+
use std::{env, fs};
170+
171+
use tempfile::{tempdir, TempDir};
172+
173+
use super::*;
174+
175+
fn setup_envs(structure: Vec<(&str, &str)>) -> (TempDir, PathBuf) {
176+
let root = tempdir().unwrap();
177+
let root_path = root.path().to_path_buf();
178+
179+
for (rel_path, content) in &structure {
180+
let dir = root_path.join(rel_path);
181+
fs::create_dir_all(&dir).unwrap();
182+
fs::write(dir.join(".env"), content).unwrap();
183+
}
184+
185+
let deepest_path = root_path.join(structure[0].0);
186+
// We MUST return root path, because dropping it will remove temp dir
187+
(root, deepest_path)
188+
}
189+
190+
#[test]
191+
fn test_load_all_single_env() {
192+
let (_root, cwd) = setup_envs(vec![("", "TEST_KEY1=VALUE1")]);
193+
194+
ForgeEnvironmentService::dot_env(&cwd);
195+
196+
assert_eq!(env::var("TEST_KEY1").unwrap(), "VALUE1");
197+
}
198+
199+
#[test]
200+
fn test_load_all_nested_envs_override() {
201+
let (_root, cwd) = setup_envs(vec![("a/b", "TEST_KEY2=SUB"), ("a", "TEST_KEY2=ROOT")]);
202+
203+
ForgeEnvironmentService::dot_env(&cwd);
204+
205+
assert_eq!(env::var("TEST_KEY2").unwrap(), "SUB");
206+
}
207+
208+
#[test]
209+
fn test_load_all_multiple_keys() {
210+
let (_root, cwd) = setup_envs(vec![
211+
("a/b", "SUB_KEY3=SUB_VAL"),
212+
("a", "ROOT_KEY3=ROOT_VAL"),
213+
]);
214+
215+
ForgeEnvironmentService::dot_env(&cwd);
216+
217+
assert_eq!(env::var("ROOT_KEY3").unwrap(), "ROOT_VAL");
218+
assert_eq!(env::var("SUB_KEY3").unwrap(), "SUB_VAL");
219+
}
220+
221+
#[test]
222+
fn test_env_precedence_std_env_wins() {
223+
let (_root, cwd) = setup_envs(vec![
224+
("a/b", "TEST_KEY4=SUB_VAL"),
225+
("a", "TEST_KEY4=ROOT_VAL"),
226+
]);
227+
228+
env::set_var("TEST_KEY4", "STD_ENV_VAL");
229+
230+
ForgeEnvironmentService::dot_env(&cwd);
231+
232+
assert_eq!(env::var("TEST_KEY4").unwrap(), "STD_ENV_VAL");
233+
}
234+
235+
#[test]
236+
fn test_custom_scenario() {
237+
let (_root, cwd) = setup_envs(vec![("a/b", "A1=1\nB1=2"), ("a", "A1=2\nC1=3")]);
238+
239+
ForgeEnvironmentService::dot_env(&cwd);
240+
241+
assert_eq!(env::var("A1").unwrap(), "1");
242+
assert_eq!(env::var("B1").unwrap(), "2");
243+
assert_eq!(env::var("C1").unwrap(), "3");
244+
}
245+
246+
#[test]
247+
fn test_custom_scenario_with_std_env_precedence() {
248+
let (_root, cwd) = setup_envs(vec![("a/b", "A2=1"), ("a", "A2=2")]);
249+
250+
env::set_var("A2", "STD_ENV");
251+
252+
ForgeEnvironmentService::dot_env(&cwd);
253+
254+
assert_eq!(env::var("A2").unwrap(), "STD_ENV");
255+
}
256+
}

0 commit comments

Comments
 (0)