From 897303cee0d87bad89cc9851a42e2127fd35b14d Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Tue, 19 May 2026 12:51:09 +0530 Subject: [PATCH 1/2] feat: honor FOREST_PATH env var for data directory Adds a FOREST_PATH environment variable that overrides `client.data_dir` for `forest`, `forest-cli`, `forest-tool` and `forest-wallet`. Precedence is env > config > defaults. The daemon also logs the resolved data directory on startup. Closes #6008 --- CHANGELOG.md | 1 + docs/docs/users/reference/env_variables.md | 1 + src/cli_shared/mod.rs | 30 ++++++++++++++++++++++ src/daemon/mod.rs | 1 + src/wallet/subcommands/wallet_cmd.rs | 11 +++++--- 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89a0b17906d8..41501b647d20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ Non-mandatory release for all node operators. It includes a few bug fixes as wel - [#6031](https://github.com/ChainSafe/forest/issues/6031): The `eth_subscribe` RPC method now supports the `pendingTransactions` subscription. - [#6012](https://github.com/ChainSafe/forest/issues/6012): Stricter validation of address arguments in `forest-wallet` subcommands. +- [#6008](https://github.com/ChainSafe/forest/issues/6008): `FOREST_PATH` environment variable to override the data directory for `forest`, `forest-cli`, `forest-tool` and `forest-wallet`. Takes precedence over `client.data_dir` in the config file. The daemon also logs the resolved data directory on startup. - [#7085](https://github.com/ChainSafe/forest/issues/7085): Implemented `nonce-fix` mpool cmd to fill mempool nonce gaps. diff --git a/docs/docs/users/reference/env_variables.md b/docs/docs/users/reference/env_variables.md index f66f218b4115..76783871b267 100644 --- a/docs/docs/users/reference/env_variables.md +++ b/docs/docs/users/reference/env_variables.md @@ -31,6 +31,7 @@ process. | `FOREST_MAX_FILTER_HEIGHT_RANGE` | positive integer | 2880 | 2880 | The maximum filter height range allowed, a conservative limit of one day | | `FOREST_STATE_MIGRATION_THREADS` | integer | Depends on the machine. | 3 | The number of threads for state migration thread-pool. Advanced users only. | | `FOREST_CONFIG_PATH` | string | /$FOREST_HOME/com.ChainSafe.Forest/config.toml | `/path/to/config.toml` | Forest configuration path. Alternatively supplied via `--config` cli parameter. | +| `FOREST_PATH` | directory path | platform-specific (`directories::ProjectDirs`) | `/var/lib/forest` | Override the Forest data directory. Honored by `forest`, `forest-cli`, `forest-tool` and `forest-wallet`. Takes precedence over the `client.data_dir` setting in the config file. | | `FOREST_TEST_RNG_FIXED_SEED` | non-negative integer | empty | 0 | Override RNG with a reproducible one seeded by the value. This should never be used out of test context for security. | | `RUST_LOG` | string | empty | `debug,forest_libp2p::service=info` | Allows for log level customization. | | `FOREST_IGNORE_DRAND` | 1 or true | empty | 1 | Ignore Drand validation. | diff --git a/src/cli_shared/mod.rs b/src/cli_shared/mod.rs index 170c032f1b6d..0f63ee601263 100644 --- a/src/cli_shared/mod.rs +++ b/src/cli_shared/mod.rs @@ -9,6 +9,17 @@ use crate::networks::NetworkChain; use crate::utils::io::read_toml; use std::path::PathBuf; +/// Environment variable that overrides the Forest data directory. +pub const FOREST_PATH_ENV: &str = "FOREST_PATH"; + +/// Returns the value of [`FOREST_PATH_ENV`] when set to a non-empty string. +pub fn forest_path_from_env() -> Option { + std::env::var(FOREST_PATH_ENV) + .ok() + .filter(|s| !s.is_empty()) + .map(PathBuf::from) +} + cfg_if::cfg_if! { if #[cfg(feature = "rustalloc")] { } else if #[cfg(feature = "jemalloc")] { @@ -37,6 +48,9 @@ pub fn read_config( if let Some(chain) = chain_opt { config.chain = chain; } + if let Some(data_dir) = forest_path_from_env() { + config.client.data_dir = data_dir; + } Ok((path, config)) } @@ -68,6 +82,22 @@ mod tests { assert_eq!(config.chain(), &NetworkChain::Butterflynet); } + #[test] + #[serial_test::serial] + fn read_config_forest_path_env_override() { + let temp_dir = tempfile::tempdir().expect("couldn't create temp dir"); + // SAFETY: tests touching the process environment are gated by `#[serial]`. + unsafe { + std::env::set_var(FOREST_PATH_ENV, temp_dir.path()); + } + let (_, config) = read_config(None, None).unwrap(); + // SAFETY: tests touching the process environment are gated by `#[serial]`. + unsafe { + std::env::remove_var(FOREST_PATH_ENV); + } + assert_eq!(config.client.data_dir, temp_dir.path()); + } + #[test] fn read_config_with_path() { let default_config = Config::default(); diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index f19b0a532292..b511837f6ba4 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -115,6 +115,7 @@ fn startup_init(config: &Config) -> anyhow::Result<()> { "Starting Forest daemon, version {}", FOREST_VERSION_STRING.as_str() ); + info!("Data directory: {}", config.client.data_dir.display()); Ok(()) } diff --git a/src/wallet/subcommands/wallet_cmd.rs b/src/wallet/subcommands/wallet_cmd.rs index ecaaa849f139..cbad930d4bf1 100644 --- a/src/wallet/subcommands/wallet_cmd.rs +++ b/src/wallet/subcommands/wallet_cmd.rs @@ -62,12 +62,15 @@ impl WalletBackend { } fn new_local(client: rpc::Client, want_encryption: bool) -> anyhow::Result { - let Some(dir) = ProjectDirs::from("com", "ChainSafe", "Forest-Wallet") else { - bail!("Failed to find wallet directory"); + let wallet_dir = if let Some(forest_path) = crate::cli_shared::forest_path_from_env() { + forest_path + } else { + let Some(dir) = ProjectDirs::from("com", "ChainSafe", "Forest-Wallet") else { + bail!("Failed to find wallet directory"); + }; + dir.data_dir().to_path_buf() }; - let wallet_dir = dir.data_dir().to_path_buf(); - let is_encrypted = wallet_dir.join(ENCRYPTED_KEYSTORE_NAME).exists(); // Always use the encrypted keystore if it exists. It it does not exist, From 905f51ad66e14a3145eeec39aa22711fb3b14c05 Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Mon, 22 Jun 2026 13:36:53 +0530 Subject: [PATCH 2/2] test: restore FOREST_PATH env on cleanup, cover empty/unset Add a drop guard so the env-override test restores any prior FOREST_PATH value (and runs cleanup on panic) instead of unconditionally unsetting it. Add unit tests covering the empty-string and unset cases of forest_path_from_env. --- src/cli_shared/mod.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/cli_shared/mod.rs b/src/cli_shared/mod.rs index 0f63ee601263..3be330d08aa1 100644 --- a/src/cli_shared/mod.rs +++ b/src/cli_shared/mod.rs @@ -82,20 +82,55 @@ mod tests { assert_eq!(config.chain(), &NetworkChain::Butterflynet); } + /// Restores [`FOREST_PATH_ENV`] to its prior value (or unsets it) on drop, + /// so cleanup runs even if the test panics. + struct EnvGuard(Option); + + impl Drop for EnvGuard { + fn drop(&mut self) { + // SAFETY: tests touching the process environment are gated by `#[serial]`. + unsafe { + match self.0.take() { + Some(prev) => std::env::set_var(FOREST_PATH_ENV, prev), + None => std::env::remove_var(FOREST_PATH_ENV), + } + } + } + } + #[test] #[serial_test::serial] fn read_config_forest_path_env_override() { let temp_dir = tempfile::tempdir().expect("couldn't create temp dir"); + let _guard = EnvGuard(std::env::var(FOREST_PATH_ENV).ok()); // SAFETY: tests touching the process environment are gated by `#[serial]`. unsafe { std::env::set_var(FOREST_PATH_ENV, temp_dir.path()); } let (_, config) = read_config(None, None).unwrap(); + assert_eq!(config.client.data_dir, temp_dir.path()); + } + + #[test] + #[serial_test::serial] + fn forest_path_from_env_empty_is_none() { + let _guard = EnvGuard(std::env::var(FOREST_PATH_ENV).ok()); + // SAFETY: tests touching the process environment are gated by `#[serial]`. + unsafe { + std::env::set_var(FOREST_PATH_ENV, ""); + } + assert!(forest_path_from_env().is_none()); + } + + #[test] + #[serial_test::serial] + fn forest_path_from_env_unset_is_none() { + let _guard = EnvGuard(std::env::var(FOREST_PATH_ENV).ok()); // SAFETY: tests touching the process environment are gated by `#[serial]`. unsafe { std::env::remove_var(FOREST_PATH_ENV); } - assert_eq!(config.client.data_dir, temp_dir.path()); + assert!(forest_path_from_env().is_none()); } #[test]