From 5134cbb049a488c239dd6a2be84b687c8789af24 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 27 Sep 2022 00:13:34 +0200 Subject: [PATCH] feat: add --skip to forge build (#3370) * feat: add --skip to forge build * feat: add skip filter * integrate filter * chore: bump ethers * test: pin version Co-authored-by: Georgios Konstantopoulos --- Cargo.lock | 48 ++--- cli/src/cmd/forge/build/filter.rs | 87 +++++++++ cli/src/cmd/forge/build/mod.rs | 54 +++++- .../fixtures/can_build_skip_contracts.stdout | 3 + cli/tests/it/cmd.rs | 20 ++ common/src/compile.rs | 173 ++++++++++-------- 6 files changed, 281 insertions(+), 104 deletions(-) create mode 100644 cli/src/cmd/forge/build/filter.rs create mode 100644 cli/tests/fixtures/can_build_skip_contracts.stdout diff --git a/Cargo.lock b/Cargo.lock index f9c25bfb0a695..414a1c584f069 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1625,7 +1625,7 @@ dependencies = [ [[package]] name = "ethers" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -1640,7 +1640,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "ethers-core", "once_cell", @@ -1651,7 +1651,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", @@ -1669,7 +1669,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "Inflector", "cfg-if 1.0.0", @@ -1692,7 +1692,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "ethers-contract-abigen", "ethers-core", @@ -1706,7 +1706,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "arrayvec 0.7.2", "bytes", @@ -1737,7 +1737,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "ethers-core", "getrandom 0.2.6", @@ -1753,7 +1753,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "async-trait", "auto_impl 0.5.0", @@ -1778,7 +1778,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "async-trait", "auto_impl 1.0.1", @@ -1815,7 +1815,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "async-trait", "coins-bip32", @@ -1838,7 +1838,7 @@ dependencies = [ [[package]] name = "ethers-solc" version = "0.17.0" -source = "git+https://github.com/gakonst/ethers-rs#afdab2a555c10d55a32c5991282b4353a4a81256" +source = "git+https://github.com/gakonst/ethers-rs#d8791482d566e2203ab6a178524f1ed6705fe274" dependencies = [ "cfg-if 1.0.0", "dunce", @@ -3231,9 +3231,9 @@ checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" [[package]] name = "md-5" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b48670c893079d3c2ed79114e3644b7004df1c361a4e0ad52e2e6940d07c3d" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ "digest 0.10.5", ] @@ -3583,9 +3583,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "oorandom" @@ -4159,9 +4159,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" dependencies = [ "unicode-ident", ] @@ -4444,9 +4444,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" dependencies = [ "base64 0.13.0", "bytes", @@ -4461,10 +4461,10 @@ dependencies = [ "hyper-tls", "ipnet", "js-sys", - "lazy_static", "log", "mime", "native-tls", + "once_cell", "percent-encoding", "pin-project-lite", "rustls", @@ -5321,18 +5321,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43" dependencies = [ "proc-macro2", "quote", diff --git a/cli/src/cmd/forge/build/filter.rs b/cli/src/cmd/forge/build/filter.rs new file mode 100644 index 0000000000000..1e984614aa86d --- /dev/null +++ b/cli/src/cmd/forge/build/filter.rs @@ -0,0 +1,87 @@ +//! Filter for excluding contracts in `forge build` + +use ethers::solc::FileFilter; +use std::{convert::Infallible, path::Path, str::FromStr}; + +/// Bundles multiple `SkipBuildFilter` into a single `FileFilter` +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SkipBuildFilters(pub Vec); + +impl FileFilter for SkipBuildFilters { + /// Only returns a match if no filter a + fn is_match(&self, file: &Path) -> bool { + self.0.iter().all(|filter| filter.is_match(file)) + } +} + +/// A filter that excludes matching contracts from the build +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum SkipBuildFilter { + /// Exclude all `.t.sol` contracts + Tests, + /// Exclude all `.s.sol` contracts + Scripts, + /// Exclude if the file matches + Custom(String), +} + +impl SkipBuildFilter { + /// Returns the pattern to match against a file + fn file_pattern(&self) -> &str { + match self { + SkipBuildFilter::Tests => ".t.sol", + SkipBuildFilter::Scripts => ".s.sol", + SkipBuildFilter::Custom(s) => s.as_str(), + } + } +} + +impl> From for SkipBuildFilter { + fn from(s: T) -> Self { + match s.as_ref() { + "tests" => SkipBuildFilter::Tests, + "scripts" => SkipBuildFilter::Scripts, + s => SkipBuildFilter::Custom(s.to_string()), + } + } +} + +impl FromStr for SkipBuildFilter { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(s.into()) + } +} + +impl FileFilter for SkipBuildFilter { + /// Matches file only if the filter does not apply + /// + /// This is returns the inverse of `file.name.contains(pattern)` + fn is_match(&self, file: &Path) -> bool { + fn exclude(file: &Path, pattern: &str) -> Option { + let file_name = file.file_name()?.to_str()?; + Some(file_name.contains(pattern)) + } + + !exclude(file, self.file_pattern()).unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_filter() { + let file = Path::new("A.t.sol"); + assert!(!SkipBuildFilter::Tests.is_match(file)); + assert!(SkipBuildFilter::Scripts.is_match(file)); + assert!(!SkipBuildFilter::Custom("A.t".to_string()).is_match(file)); + + let file = Path::new("A.s.sol"); + assert!(SkipBuildFilter::Tests.is_match(file)); + assert!(!SkipBuildFilter::Scripts.is_match(file)); + assert!(!SkipBuildFilter::Custom("A.s".to_string()).is_match(file)); + } +} diff --git a/cli/src/cmd/forge/build/mod.rs b/cli/src/cmd/forge/build/mod.rs index 8a81452ff825f..b9540f00c1638 100644 --- a/cli/src/cmd/forge/build/mod.rs +++ b/cli/src/cmd/forge/build/mod.rs @@ -1,14 +1,15 @@ //! Build command use crate::cmd::{ forge::{ + build::filter::{SkipBuildFilter, SkipBuildFilters}, install::{self}, watch::WatchArgs, }, Cmd, LoadConfig, }; -use clap::Parser; +use clap::{ArgAction, Parser}; use ethers::solc::{Project, ProjectCompileOutput}; -use foundry_common::compile; +use foundry_common::{compile, compile::ProjectCompiler}; use foundry_config::{ figment::{ self, @@ -19,6 +20,7 @@ use foundry_config::{ Config, }; use serde::Serialize; +use tracing::trace; use watchexec::config::{InitConfig, RuntimeConfig}; mod core; @@ -27,6 +29,8 @@ pub use self::core::CoreBuildArgs; mod paths; pub use self::paths::ProjectPathsArgs; +mod filter; + foundry_config::merge_impl_figment_convert!(BuildArgs, args); /// All `forge build` related arguments @@ -64,6 +68,14 @@ pub struct BuildArgs { #[serde(skip)] pub sizes: bool, + #[clap( + long, + multiple_values = true, + action = ArgAction::Append, + help = "Skip building whose names contain FILTER. `tests` and `scripts` are aliases for `.t.sol` and `.s.sol`. (this flag can be used multiple times)")] + #[serde(skip)] + pub skip: Option>, + #[clap(flatten, next_help_heading = "WATCH OPTIONS")] #[serde(skip)] pub watch: WatchArgs, @@ -83,10 +95,23 @@ impl Cmd for BuildArgs { project = config.project()?; } + let filters = self.skip.unwrap_or_default(); + if self.args.silent { - compile::suppress_compile(&project) + if filters.is_empty() { + compile::suppress_compile(&project) + } else { + trace!(?filters, "compile with filters suppressed"); + compile::suppress_compile_sparse(&project, SkipBuildFilters(filters)) + } } else { - compile::compile(&project, self.names, self.sizes) + let compiler = ProjectCompiler::new(self.names, self.sizes); + if filters.is_empty() { + compiler.compile(&project) + } else { + trace!(?filters, "compile with filters"); + compiler.compile_sparse(&project, SkipBuildFilters(filters)) + } } } } @@ -139,3 +164,24 @@ impl Provider for BuildArgs { Ok(Map::from([(Config::selected_profile(), dict)])) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_parse_build_filters() { + let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests"]); + assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests])); + + let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "scripts"]); + assert_eq!(args.skip, Some(vec![SkipBuildFilter::Scripts])); + + let args: BuildArgs = + BuildArgs::parse_from(["foundry-cli", "--skip", "tests", "--skip", "scripts"]); + assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts])); + + let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests", "scripts"]); + assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts])); + } +} diff --git a/cli/tests/fixtures/can_build_skip_contracts.stdout b/cli/tests/fixtures/can_build_skip_contracts.stdout new file mode 100644 index 0000000000000..5b031a27a4b25 --- /dev/null +++ b/cli/tests/fixtures/can_build_skip_contracts.stdout @@ -0,0 +1,3 @@ +Compiling 1 files with 0.8.17 +Solc 0.8.17 finished in 34.45ms +Compiler run successful diff --git a/cli/tests/it/cmd.rs b/cli/tests/it/cmd.rs index 35207afe67135..a1b17a954c130 100644 --- a/cli/tests/it/cmd.rs +++ b/cli/tests/it/cmd.rs @@ -1341,3 +1341,23 @@ forgetest_init!(can_install_missing_deps_build, |prj: TestProject, mut cmd: Test assert!(output.contains("Missing dependencies found. Installing now"), "{}", output); assert!(output.contains("Compiler run successful"), "{}", output); }); + +// checks that extra output works +forgetest_init!(can_build_skip_contracts, |prj: TestProject, mut cmd: TestCommand| { + // explicitly set to run with 0.8.17 for consistent output + let config = Config { solc: Some("0.8.17".into()), ..Default::default() }; + prj.write_config(config); + + // only builds the single template contract `src/*` + cmd.args(["build", "--skip", "tests", "--skip", "scripts"]); + + cmd.unchecked_output().stdout_matches_path( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/can_build_skip_contracts.stdout"), + ); + // re-run command + let out = cmd.stdout(); + + // unchanged + assert!(out.trim().contains("No files changed, compilation skipped"), "{}", out); +}); diff --git a/common/src/compile.rs b/common/src/compile.rs index 31551c06bd380..22bfdfced3554 100644 --- a/common/src/compile.rs +++ b/common/src/compile.rs @@ -16,82 +16,6 @@ use std::{ }; use tempfile::NamedTempFile; -/// Compiles the provided [`Project`], throws if there's any compiler error and logs whether -/// compilation was successful or if there was a cache hit. -pub fn compile( - project: &Project, - print_names: bool, - print_sizes: bool, -) -> eyre::Result { - ProjectCompiler::new(print_names, print_sizes).compile(project) -} - -// https://eips.ethereum.org/EIPS/eip-170 -const CONTRACT_SIZE_LIMIT: usize = 24576; - -/// Contracts with info about their size -pub struct SizeReport { - /// `:info>` - pub contracts: BTreeMap, -} - -/// How big the contract is and whether it is a dev contract where size limits can be neglected -pub struct ContractInfo { - /// size of the contract in bytes - pub size: usize, - /// A development contract is either a Script or a Test contract. - pub is_dev_contract: bool, -} - -impl SizeReport { - /// Returns the size of the largest contract, excluding test contracts. - pub fn max_size(&self) -> usize { - let mut max_size = 0; - for contract in self.contracts.values() { - if !contract.is_dev_contract && contract.size > max_size { - max_size = contract.size; - } - } - max_size - } - - /// Returns true if any contract exceeds the size limit, excluding test contracts. - pub fn exceeds_size_limit(&self) -> bool { - self.max_size() > CONTRACT_SIZE_LIMIT - } -} - -impl Display for SizeReport { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - let mut table = Table::new(); - table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS); - table.set_header(vec![ - Cell::new("Contract").add_attribute(Attribute::Bold).fg(Color::Blue), - Cell::new("Size (kB)").add_attribute(Attribute::Bold).fg(Color::Blue), - Cell::new("Margin (kB)").add_attribute(Attribute::Bold).fg(Color::Blue), - ]); - - let contracts = self.contracts.iter().filter(|(_, c)| !c.is_dev_contract && c.size > 0); - for (name, contract) in contracts { - let margin = CONTRACT_SIZE_LIMIT as isize - contract.size as isize; - let color = match contract.size { - 0..=17999 => Color::Reset, - 18000..=CONTRACT_SIZE_LIMIT => Color::Yellow, - _ => Color::Red, - }; - - table.add_row(vec![ - Cell::new(name).fg(color), - Cell::new(contract.size as f64 / 1000.0).fg(color), - Cell::new(margin as f64 / 1000.0).fg(color), - ]); - } - - writeln!(f, "{}", table)?; - Ok(()) - } -} - /// Helper type to configure how to compile a project /// /// This is merely a wrapper for [Project::compile()] which also prints to stdout dependent on its @@ -221,6 +145,82 @@ impl ProjectCompiler { } } +// https://eips.ethereum.org/EIPS/eip-170 +const CONTRACT_SIZE_LIMIT: usize = 24576; + +/// Contracts with info about their size +pub struct SizeReport { + /// `:info>` + pub contracts: BTreeMap, +} + +impl SizeReport { + /// Returns the size of the largest contract, excluding test contracts. + pub fn max_size(&self) -> usize { + let mut max_size = 0; + for contract in self.contracts.values() { + if !contract.is_dev_contract && contract.size > max_size { + max_size = contract.size; + } + } + max_size + } + + /// Returns true if any contract exceeds the size limit, excluding test contracts. + pub fn exceeds_size_limit(&self) -> bool { + self.max_size() > CONTRACT_SIZE_LIMIT + } +} + +impl Display for SizeReport { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + let mut table = Table::new(); + table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS); + table.set_header(vec![ + Cell::new("Contract").add_attribute(Attribute::Bold).fg(Color::Blue), + Cell::new("Size (kB)").add_attribute(Attribute::Bold).fg(Color::Blue), + Cell::new("Margin (kB)").add_attribute(Attribute::Bold).fg(Color::Blue), + ]); + + let contracts = self.contracts.iter().filter(|(_, c)| !c.is_dev_contract && c.size > 0); + for (name, contract) in contracts { + let margin = CONTRACT_SIZE_LIMIT as isize - contract.size as isize; + let color = match contract.size { + 0..=17999 => Color::Reset, + 18000..=CONTRACT_SIZE_LIMIT => Color::Yellow, + _ => Color::Red, + }; + + table.add_row(vec![ + Cell::new(name).fg(color), + Cell::new(contract.size as f64 / 1000.0).fg(color), + Cell::new(margin as f64 / 1000.0).fg(color), + ]); + } + + writeln!(f, "{}", table)?; + Ok(()) + } +} + +/// How big the contract is and whether it is a dev contract where size limits can be neglected +pub struct ContractInfo { + /// size of the contract in bytes + pub size: usize, + /// A development contract is either a Script or a Test contract. + pub is_dev_contract: bool, +} + +/// Compiles the provided [`Project`], throws if there's any compiler error and logs whether +/// compilation was successful or if there was a cache hit. +pub fn compile( + project: &Project, + print_names: bool, + print_sizes: bool, +) -> eyre::Result { + ProjectCompiler::new(print_names, print_sizes).compile(project) +} + /// Compiles the provided [`Project`], throws if there's any compiler error and logs whether /// compilation was successful or if there was a cache hit. /// Doesn't print anything to stdout, thus is "suppressed". @@ -237,6 +237,27 @@ pub fn suppress_compile(project: &Project) -> eyre::Result Ok(output) } +/// Compiles the provided [`Project`], throws if there's any compiler error and logs whether +/// compilation was successful or if there was a cache hit. +/// Doesn't print anything to stdout, thus is "suppressed". +/// +/// See [`Project::compile_sparse`] +pub fn suppress_compile_sparse( + project: &Project, + filter: F, +) -> eyre::Result { + let output = ethers_solc::report::with_scoped( + ðers_solc::report::Report::new(NoReporter::default()), + || project.compile_sparse(filter), + )?; + + if output.has_compiler_errors() { + eyre::bail!(output.to_string()) + } + + Ok(output) +} + /// Compile a set of files not necessarily included in the `project`'s source dir /// /// If `silent` no solc related output will be emitted to stdout