diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 27e5f85059a59..c0f8249f37dfd 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -45,7 +45,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::{ borrow::Cow, collections::BTreeMap, - fs, + fs, io, path::{Path, PathBuf}, str::FromStr, }; @@ -1269,7 +1269,9 @@ impl Config { let project = builder.build(self.compiler()?)?; if self.force { - self.cleanup(&project)?; + // Warnings are intentionally dropped here because `sh_warn!` is a circular + // dependency. Callers that need warnings should call `cleanup()` directly. + let _ = self.cleanup(&project); } Ok(project) @@ -1298,21 +1300,40 @@ impl Config { } /// Cleans the project. + /// + /// Returns a list of warning messages for any non-fatal cleanup failures. Cleanup is + /// best-effort: all steps are attempted even if some fail. pub fn cleanup>( &self, project: &Project, - ) -> Result<(), SolcError> { - project.cleanup()?; + ) -> Result, SolcError> { + let mut warnings = Vec::new(); + + if let Err(err) = project.cleanup() { + warnings.push(format!("failed to clean project artifacts: {err}")); + } // Remove last test run failures file. - let _ = fs::remove_file(&self.test_failures_file); + if let Err(err) = fs::remove_file(&self.test_failures_file) + && err.kind() != io::ErrorKind::NotFound + { + warnings.push(format!( + "failed to remove test failures file {}: {err}", + self.test_failures_file.display() + )); + } // Remove fuzz and invariant cache directories. - let remove_test_dir = |test_dir: &Option| { + let mut remove_test_dir = |test_dir: &Option| { if let Some(test_dir) = test_dir { let path = project.root().join(test_dir); - if path.exists() { - let _ = fs::remove_dir_all(&path); + if let Err(err) = fs::remove_dir_all(&path) + && err.kind() != io::ErrorKind::NotFound + { + warnings.push(format!( + "failed to remove test cache directory {}: {err}", + path.display() + )); } } }; @@ -1321,7 +1342,7 @@ impl Config { remove_test_dir(&self.invariant.corpus.corpus_dir); remove_test_dir(&self.invariant.failure_persist_dir); - Ok(()) + Ok(warnings) } /// Ensures that the configured version is installed if explicitly set @@ -2154,63 +2175,108 @@ impl Config { } /// Clears the foundry cache. - pub fn clean_foundry_cache() -> eyre::Result<()> { + /// + /// Returns warnings for any non-fatal deletion failures. + pub fn clean_foundry_cache() -> eyre::Result> { if let Some(cache_dir) = Self::foundry_cache_dir() { let path = cache_dir.as_path(); - let _ = fs::remove_dir_all(path); + if let Err(err) = fs::remove_dir_all(path) + && err.kind() != io::ErrorKind::NotFound + { + return Ok(vec![format!( + "failed to remove foundry cache at {}: {err}", + path.display() + )]); + } } else { eyre::bail!("failed to get foundry_cache_dir"); } - Ok(()) + Ok(vec![]) } /// Clears the foundry cache for `chain`. - pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> { + /// + /// Returns warnings for any non-fatal deletion failures. + pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result> { if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) { let path = cache_dir.as_path(); - let _ = fs::remove_dir_all(path); + if let Err(err) = fs::remove_dir_all(path) + && err.kind() != io::ErrorKind::NotFound + { + return Ok(vec![format!( + "failed to remove foundry cache for chain {chain} at {}: {err}", + path.display() + )]); + } } else { eyre::bail!("failed to get foundry_chain_cache_dir"); } - Ok(()) + Ok(vec![]) } /// Clears the foundry cache for `chain` and `block`. - pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> { + /// + /// Returns warnings for any non-fatal deletion failures. + pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result> { if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) { let path = cache_dir.as_path(); - let _ = fs::remove_dir_all(path); + if let Err(err) = fs::remove_dir_all(path) + && err.kind() != io::ErrorKind::NotFound + { + return Ok(vec![format!( + "failed to remove foundry cache for chain {chain} block {block} at {}: {err}", + path.display() + )]); + } } else { eyre::bail!("failed to get foundry_block_cache_dir"); } - Ok(()) + Ok(vec![]) } /// Clears the foundry etherscan cache. - pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> { + /// + /// Returns warnings for any non-fatal deletion failures. + pub fn clean_foundry_etherscan_cache() -> eyre::Result> { if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() { let path = cache_dir.as_path(); - let _ = fs::remove_dir_all(path); + if let Err(err) = fs::remove_dir_all(path) + && err.kind() != io::ErrorKind::NotFound + { + return Ok(vec![format!( + "failed to remove foundry etherscan cache at {}: {err}", + path.display() + )]); + } } else { eyre::bail!("failed to get foundry_etherscan_cache_dir"); } - Ok(()) + Ok(vec![]) } /// Clears the foundry etherscan cache for `chain`. - pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> { + /// + /// Returns warnings for any non-fatal deletion failures. + pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result> { if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) { let path = cache_dir.as_path(); - let _ = fs::remove_dir_all(path); + if let Err(err) = fs::remove_dir_all(path) + && err.kind() != io::ErrorKind::NotFound + { + return Ok(vec![format!( + "failed to remove foundry etherscan cache for chain {chain} at {}: {err}", + path.display() + )]); + } } else { eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain); } - Ok(()) + Ok(vec![]) } /// List the data in the foundry cache. diff --git a/crates/forge/src/args.rs b/crates/forge/src/args.rs index 4f55f028a3690..c5779aebbcbc7 100644 --- a/crates/forge/src/args.rs +++ b/crates/forge/src/args.rs @@ -6,7 +6,7 @@ use clap::{CommandFactory, Parser}; use clap_complete::generate; use eyre::Result; use foundry_cli::utils; -use foundry_common::shell; +use foundry_common::{sh_warn, shell}; use foundry_evm::inspectors::cheatcodes::{ForgeContext, set_execution_context}; /// Run the `forge` command line interface. @@ -99,7 +99,9 @@ pub fn run_command(args: Forge) -> Result<()> { ForgeSubcommand::Clean { root } => { let config = utils::load_config_with_root(root.as_deref())?; let project = config.project()?; - config.cleanup(&project)?; + for warning in config.cleanup(&project)? { + let _ = sh_warn!("{warning}"); + } Ok(()) } ForgeSubcommand::Snapshot(cmd) => { diff --git a/crates/forge/src/cmd/cache.rs b/crates/forge/src/cmd/cache.rs index 432d7c11c19dd..0c1f22f2e6de6 100644 --- a/crates/forge/src/cmd/cache.rs +++ b/crates/forge/src/cmd/cache.rs @@ -4,6 +4,7 @@ use clap::{ builder::{PossibleValuesParser, TypedValueParser}, }; use eyre::Result; +use foundry_common::sh_warn; use foundry_config::{Chain, Config, NamedChain, cache}; use std::{ffi::OsStr, str::FromStr}; use strum::VariantNames; @@ -63,10 +64,13 @@ impl CleanArgs { clean_chain_cache(chain, blocks.clone(), etherscan)? } ChainOrAll::All => { - if etherscan { - Config::clean_foundry_etherscan_cache()?; + let warnings = if etherscan { + Config::clean_foundry_etherscan_cache()? } else { Config::clean_foundry_cache()? + }; + for warning in warnings { + let _ = sh_warn!("{warning}"); } } } @@ -128,17 +132,24 @@ impl FromStr for ChainOrAll { fn clean_chain_cache(chain: impl Into, blocks: Vec, etherscan: bool) -> Result<()> { let chain = chain.into(); + let mut warnings = Vec::new(); if blocks.is_empty() { - Config::clean_foundry_etherscan_chain_cache(chain)?; + warnings.extend(Config::clean_foundry_etherscan_chain_cache(chain)?); if etherscan { + for warning in warnings { + let _ = sh_warn!("{warning}"); + } return Ok(()); } - Config::clean_foundry_chain_cache(chain)?; + warnings.extend(Config::clean_foundry_chain_cache(chain)?); } else { for block in blocks { - Config::clean_foundry_block_cache(chain, block)?; + warnings.extend(Config::clean_foundry_block_cache(chain, block)?); } } + for warning in warnings { + let _ = sh_warn!("{warning}"); + } Ok(()) }