Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions crates/forge_infra/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ use tokio::sync::Mutex;

use crate::console::StdConsoleWriter;

/// Environment variables that let supported shells select startup files before
/// executing the requested command.
const SHELL_STARTUP_ENV_VARS: [&str; 3] = ["BASH_ENV", "ENV", "ZDOTDIR"];

/// Service for executing shell commands
#[derive(Clone, Debug)]
pub struct ForgeCommandExecutorService {
Expand Down Expand Up @@ -87,6 +91,12 @@ impl ForgeCommandExecutorService {
}
}

// Project `.env` files are loaded into Forge's process environment.
// Do not let them configure shell startup behavior for subprocesses.
for env_var in SHELL_STARTUP_ENV_VARS {
command.env_remove(env_var);
}

command
}

Expand Down Expand Up @@ -273,6 +283,7 @@ impl CommandInfra for ForgeCommandExecutorService {
mod tests {

use pretty_assertions::assert_eq;
use serial_test::serial;

use super::*;

Expand All @@ -293,6 +304,32 @@ mod tests {
Arc::new(StdConsoleWriter::default())
}

#[cfg(unix)]
struct EnvVarGuard {
key: &'static str,
previous: Option<std::ffi::OsString>,
}

#[cfg(unix)]
impl EnvVarGuard {
fn set(key: &'static str, value: &Path) -> Self {
let previous = std::env::var_os(key);
unsafe { std::env::set_var(key, value) };
Self { key, previous }
}
}

#[cfg(unix)]
impl Drop for EnvVarGuard {
fn drop(&mut self) {
if let Some(previous) = &self.previous {
unsafe { std::env::set_var(self.key, previous) };
} else {
unsafe { std::env::remove_var(self.key) };
}
}
}

#[tokio::test]
async fn test_command_executor() {
let fixture = ForgeCommandExecutorService::new(test_env(), test_printer());
Expand All @@ -319,6 +356,64 @@ mod tests {
assert_eq!(actual.stderr, expected.stderr);
assert_eq!(actual.success(), expected.success());
}

#[cfg(unix)]
#[tokio::test]
#[serial]
async fn test_command_executor_does_not_source_bash_env() {
let fixture_dir = tempfile::tempdir().unwrap();
let marker = fixture_dir.path().join("marker.txt");
let startup = fixture_dir.path().join("startup.sh");
std::fs::write(&startup, "touch marker.txt\n").unwrap();
let _guard = EnvVarGuard::set("BASH_ENV", &startup);
let fixture = ForgeCommandExecutorService::new(test_env(), test_printer());

let actual = fixture
.execute_command(
"echo ok".to_string(),
fixture_dir.path().to_path_buf(),
true,
None,
)
.await
.unwrap();
assert!(actual.success());
assert!(
!marker.exists(),
"BASH_ENV startup script ran before the requested command"
);
}

#[test]
fn test_prepare_command_removes_shell_startup_environment() {
let fixture = ForgeCommandExecutorService::new(test_env(), test_printer());

let command = fixture.prepare_command("echo ok", Path::new("."), None);
let mut actual = command
.as_std()
.get_envs()
.filter(|(key, _)| {
SHELL_STARTUP_ENV_VARS
.iter()
.any(|candidate| *key == std::ffi::OsStr::new(candidate))
})
.map(|(key, value)| {
(
key.to_string_lossy().into_owned(),
value.map(|value| value.to_string_lossy().into_owned()),
)
})
.collect::<Vec<_>>();
actual.sort();
let expected = vec![
("BASH_ENV".to_string(), None),
("ENV".to_string(), None),
("ZDOTDIR".to_string(), None),
];

assert_eq!(actual, expected);
}

#[tokio::test]
async fn test_command_executor_with_env_vars_success() {
// Set up test environment variables
Expand Down
Loading