Skip to content

Commit 279ec19

Browse files
authored
feat(config): allow configuration of the base_path (#2864)
1 parent 58d0df9 commit 279ec19

6 files changed

Lines changed: 75 additions & 15 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ System-level environment variables (usually set automatically):
558558

559559
```bash
560560
# .env
561+
FORGE_CONFIG=/custom/config/dir # Base directory for all Forge config files (default: ~/forge)
561562
FORGE_MAX_SEARCH_RESULT_BYTES=10240 # Maximum bytes for search results (default: 10240 - 10 KB)
562563
FORGE_HISTORY_FILE=/path/to/history # Custom path for Forge history file (default: uses system default location)
563564
FORGE_BANNER="Your custom banner text" # Custom banner text to display on startup (default: Forge ASCII art)

crates/forge_app/src/orch_spec/orch_setup.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ impl Default for TestContext {
5656
attachments: Default::default(),
5757
env: Environment {
5858
os: "MacOS".to_string(),
59-
pid: 1234,
6059
cwd: PathBuf::from("/Users/tushar"),
6160
home: Some(PathBuf::from("/Users/tushar")),
6261
shell: "bash".to_string(),

crates/forge_config/src/reader.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,14 @@ impl ConfigReader {
4949
Self::base_path().join(".forge.toml")
5050
}
5151

52-
/// Returns the base directory for all Forge config files (`~/forge`).
52+
/// Returns the base directory for all Forge config files.
53+
///
54+
/// If the `FORGE_CONFIG` environment variable is set, its value is used
55+
/// directly as the base path. Otherwise defaults to `~/forge`.
5356
pub fn base_path() -> PathBuf {
57+
if let Ok(path) = std::env::var("FORGE_CONFIG") {
58+
return PathBuf::from(path);
59+
}
5460
dirs::home_dir().unwrap_or(PathBuf::from(".")).join("forge")
5561
}
5662

@@ -164,6 +170,21 @@ mod tests {
164170
}
165171
}
166172

173+
#[test]
174+
fn test_base_path_uses_forge_config_env_var() {
175+
let _guard = EnvGuard::set(&[("FORGE_CONFIG", "/custom/forge/dir")]);
176+
let actual = ConfigReader::base_path();
177+
let expected = PathBuf::from("/custom/forge/dir");
178+
assert_eq!(actual, expected);
179+
}
180+
181+
#[test]
182+
fn test_base_path_falls_back_to_home_dir_when_env_var_absent() {
183+
let actual = ConfigReader::base_path();
184+
// Without FORGE_CONFIG set the path must end with "forge"
185+
assert_eq!(actual.file_name().unwrap(), "forge");
186+
}
187+
167188
#[test]
168189
fn test_read_parses_without_error() {
169190
let actual = ConfigReader::default().read_defaults().build();

crates/forge_domain/src/env.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ const VERSION: &str = match option_env!("APP_VERSION") {
4747
/// Represents the minimal runtime environment in which the application is
4848
/// running.
4949
///
50-
/// Contains only the six fields that cannot be sourced from [`ForgeConfig`]:
51-
/// `os`, `pid`, `cwd`, `home`, `shell`, and `base_path`. All configuration
50+
/// Contains only the five fields that cannot be sourced from [`ForgeConfig`]:
51+
/// `os`, `cwd`, `home`, `shell`, and `base_path`. All configuration
5252
/// values previously carried here are now accessed through
5353
/// `EnvironmentInfra::get_config()`.
5454
#[derive(Debug, Setters, Clone, PartialEq, Serialize, Deserialize, fake::Dummy)]
@@ -57,8 +57,6 @@ const VERSION: &str = match option_env!("APP_VERSION") {
5757
pub struct Environment {
5858
/// The operating system of the environment.
5959
pub os: String,
60-
/// The process ID of the current process.
61-
pub pid: u32,
6260
/// The current working directory.
6361
pub cwd: PathBuf,
6462
/// The home directory.

crates/forge_infra/src/env.rs

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,20 @@ use tracing::debug;
99

1010
/// Builds a [`forge_domain::Environment`] from runtime context only.
1111
///
12-
/// Only the six fields that cannot be sourced from [`ForgeConfig`] are set
13-
/// here: `os`, `pid`, `cwd`, `home`, `shell`, and `base_path`. All
14-
/// configuration values are now accessed through
15-
/// `EnvironmentInfra::get_config()`.
12+
/// Only the five fields that cannot be sourced from [`ForgeConfig`] are set
13+
/// here: `os`, `cwd`, `home`, `shell`, and `base_path`. All configuration
14+
/// values are now accessed through `EnvironmentInfra::get_config()`.
1615
pub fn to_environment(cwd: PathBuf) -> Environment {
1716
Environment {
1817
os: std::env::consts::OS.to_string(),
19-
pid: std::process::id(),
2018
cwd,
2119
home: dirs::home_dir(),
2220
shell: if cfg!(target_os = "windows") {
2321
std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string())
2422
} else {
2523
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
2624
},
27-
base_path: dirs::home_dir()
28-
.map(|h| h.join("forge"))
29-
.unwrap_or_else(|| PathBuf::from(".").join("forge")),
25+
base_path: ConfigReader::base_path(),
3026
}
3127
}
3228

@@ -172,19 +168,65 @@ impl EnvironmentInfra for ForgeEnvironmentInfra {
172168
#[cfg(test)]
173169
mod tests {
174170
use std::path::PathBuf;
171+
use std::sync::{Mutex, MutexGuard};
175172

176173
use forge_config::ForgeConfig;
177174
use pretty_assertions::assert_eq;
178175

179176
use super::*;
180177

178+
/// Serializes tests that mutate environment variables to prevent races.
179+
static ENV_MUTEX: Mutex<()> = Mutex::new(());
180+
181+
/// Holds env vars set for a test's duration and removes them on drop,
182+
/// while holding [`ENV_MUTEX`].
183+
struct EnvGuard {
184+
keys: Vec<&'static str>,
185+
_lock: MutexGuard<'static, ()>,
186+
}
187+
188+
impl EnvGuard {
189+
#[must_use]
190+
fn set(pairs: &[(&'static str, &str)]) -> Self {
191+
let lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
192+
let keys = pairs.iter().map(|(k, _)| *k).collect();
193+
for (key, value) in pairs {
194+
unsafe { std::env::set_var(key, value) };
195+
}
196+
Self { keys, _lock: lock }
197+
}
198+
}
199+
200+
impl Drop for EnvGuard {
201+
fn drop(&mut self) {
202+
for key in &self.keys {
203+
unsafe { std::env::remove_var(key) };
204+
}
205+
}
206+
}
207+
181208
#[test]
182209
fn test_to_environment_sets_cwd() {
183210
let fixture_cwd = PathBuf::from("/test/cwd");
184211
let actual = to_environment(fixture_cwd.clone());
185212
assert_eq!(actual.cwd, fixture_cwd);
186213
}
187214

215+
#[test]
216+
fn test_to_environment_uses_forge_config_env_var() {
217+
let _guard = EnvGuard::set(&[("FORGE_CONFIG", "/custom/config/dir")]);
218+
let actual = to_environment(PathBuf::from("/any/cwd"));
219+
let expected = PathBuf::from("/custom/config/dir");
220+
assert_eq!(actual.base_path, expected);
221+
}
222+
223+
#[test]
224+
fn test_to_environment_falls_back_to_home_dir_when_env_var_absent() {
225+
let actual = to_environment(PathBuf::from("/any/cwd"));
226+
// Without FORGE_CONFIG the base_path must end with "forge"
227+
assert_eq!(actual.base_path.file_name().unwrap(), "forge");
228+
}
229+
188230
#[test]
189231
fn test_apply_config_op_set_provider() {
190232
use forge_domain::ProviderId;

crates/forge_services/src/app_config.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,6 @@ mod tests {
267267
fn get_environment(&self) -> Environment {
268268
Environment {
269269
os: "test".to_string(),
270-
pid: 0,
271270
cwd: PathBuf::new(),
272271
home: None,
273272
shell: "bash".to_string(),

0 commit comments

Comments
 (0)