Skip to content

Commit 226affb

Browse files
mattssegakonst
andauthored
feat: add --skip <filter> to forge build (#3370)
* feat: add --skip <filter> to forge build * feat: add skip filter * integrate filter * chore: bump ethers * test: pin version Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
1 parent ec5cc47 commit 226affb

File tree

6 files changed

+281
-104
lines changed

6 files changed

+281
-104
lines changed

Cargo.lock

+24-24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/cmd/forge/build/filter.rs

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Filter for excluding contracts in `forge build`
2+
3+
use ethers::solc::FileFilter;
4+
use std::{convert::Infallible, path::Path, str::FromStr};
5+
6+
/// Bundles multiple `SkipBuildFilter` into a single `FileFilter`
7+
#[derive(Debug, Clone, Eq, PartialEq)]
8+
pub struct SkipBuildFilters(pub Vec<SkipBuildFilter>);
9+
10+
impl FileFilter for SkipBuildFilters {
11+
/// Only returns a match if no filter a
12+
fn is_match(&self, file: &Path) -> bool {
13+
self.0.iter().all(|filter| filter.is_match(file))
14+
}
15+
}
16+
17+
/// A filter that excludes matching contracts from the build
18+
#[derive(Debug, Clone, Eq, PartialEq)]
19+
pub enum SkipBuildFilter {
20+
/// Exclude all `.t.sol` contracts
21+
Tests,
22+
/// Exclude all `.s.sol` contracts
23+
Scripts,
24+
/// Exclude if the file matches
25+
Custom(String),
26+
}
27+
28+
impl SkipBuildFilter {
29+
/// Returns the pattern to match against a file
30+
fn file_pattern(&self) -> &str {
31+
match self {
32+
SkipBuildFilter::Tests => ".t.sol",
33+
SkipBuildFilter::Scripts => ".s.sol",
34+
SkipBuildFilter::Custom(s) => s.as_str(),
35+
}
36+
}
37+
}
38+
39+
impl<T: AsRef<str>> From<T> for SkipBuildFilter {
40+
fn from(s: T) -> Self {
41+
match s.as_ref() {
42+
"tests" => SkipBuildFilter::Tests,
43+
"scripts" => SkipBuildFilter::Scripts,
44+
s => SkipBuildFilter::Custom(s.to_string()),
45+
}
46+
}
47+
}
48+
49+
impl FromStr for SkipBuildFilter {
50+
type Err = Infallible;
51+
52+
fn from_str(s: &str) -> Result<Self, Self::Err> {
53+
Ok(s.into())
54+
}
55+
}
56+
57+
impl FileFilter for SkipBuildFilter {
58+
/// Matches file only if the filter does not apply
59+
///
60+
/// This is returns the inverse of `file.name.contains(pattern)`
61+
fn is_match(&self, file: &Path) -> bool {
62+
fn exclude(file: &Path, pattern: &str) -> Option<bool> {
63+
let file_name = file.file_name()?.to_str()?;
64+
Some(file_name.contains(pattern))
65+
}
66+
67+
!exclude(file, self.file_pattern()).unwrap_or_default()
68+
}
69+
}
70+
71+
#[cfg(test)]
72+
mod tests {
73+
use super::*;
74+
75+
#[test]
76+
fn test_build_filter() {
77+
let file = Path::new("A.t.sol");
78+
assert!(!SkipBuildFilter::Tests.is_match(file));
79+
assert!(SkipBuildFilter::Scripts.is_match(file));
80+
assert!(!SkipBuildFilter::Custom("A.t".to_string()).is_match(file));
81+
82+
let file = Path::new("A.s.sol");
83+
assert!(SkipBuildFilter::Tests.is_match(file));
84+
assert!(!SkipBuildFilter::Scripts.is_match(file));
85+
assert!(!SkipBuildFilter::Custom("A.s".to_string()).is_match(file));
86+
}
87+
}

cli/src/cmd/forge/build/mod.rs

+50-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
//! Build command
22
use crate::cmd::{
33
forge::{
4+
build::filter::{SkipBuildFilter, SkipBuildFilters},
45
install::{self},
56
watch::WatchArgs,
67
},
78
Cmd, LoadConfig,
89
};
9-
use clap::Parser;
10+
use clap::{ArgAction, Parser};
1011
use ethers::solc::{Project, ProjectCompileOutput};
11-
use foundry_common::compile;
12+
use foundry_common::{compile, compile::ProjectCompiler};
1213
use foundry_config::{
1314
figment::{
1415
self,
@@ -19,6 +20,7 @@ use foundry_config::{
1920
Config,
2021
};
2122
use serde::Serialize;
23+
use tracing::trace;
2224
use watchexec::config::{InitConfig, RuntimeConfig};
2325

2426
mod core;
@@ -27,6 +29,8 @@ pub use self::core::CoreBuildArgs;
2729
mod paths;
2830
pub use self::paths::ProjectPathsArgs;
2931

32+
mod filter;
33+
3034
foundry_config::merge_impl_figment_convert!(BuildArgs, args);
3135

3236
/// All `forge build` related arguments
@@ -64,6 +68,14 @@ pub struct BuildArgs {
6468
#[serde(skip)]
6569
pub sizes: bool,
6670

71+
#[clap(
72+
long,
73+
multiple_values = true,
74+
action = ArgAction::Append,
75+
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)")]
76+
#[serde(skip)]
77+
pub skip: Option<Vec<SkipBuildFilter>>,
78+
6779
#[clap(flatten, next_help_heading = "WATCH OPTIONS")]
6880
#[serde(skip)]
6981
pub watch: WatchArgs,
@@ -83,10 +95,23 @@ impl Cmd for BuildArgs {
8395
project = config.project()?;
8496
}
8597

98+
let filters = self.skip.unwrap_or_default();
99+
86100
if self.args.silent {
87-
compile::suppress_compile(&project)
101+
if filters.is_empty() {
102+
compile::suppress_compile(&project)
103+
} else {
104+
trace!(?filters, "compile with filters suppressed");
105+
compile::suppress_compile_sparse(&project, SkipBuildFilters(filters))
106+
}
88107
} else {
89-
compile::compile(&project, self.names, self.sizes)
108+
let compiler = ProjectCompiler::new(self.names, self.sizes);
109+
if filters.is_empty() {
110+
compiler.compile(&project)
111+
} else {
112+
trace!(?filters, "compile with filters");
113+
compiler.compile_sparse(&project, SkipBuildFilters(filters))
114+
}
90115
}
91116
}
92117
}
@@ -139,3 +164,24 @@ impl Provider for BuildArgs {
139164
Ok(Map::from([(Config::selected_profile(), dict)]))
140165
}
141166
}
167+
168+
#[cfg(test)]
169+
mod tests {
170+
use super::*;
171+
172+
#[test]
173+
fn can_parse_build_filters() {
174+
let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests"]);
175+
assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests]));
176+
177+
let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "scripts"]);
178+
assert_eq!(args.skip, Some(vec![SkipBuildFilter::Scripts]));
179+
180+
let args: BuildArgs =
181+
BuildArgs::parse_from(["foundry-cli", "--skip", "tests", "--skip", "scripts"]);
182+
assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts]));
183+
184+
let args: BuildArgs = BuildArgs::parse_from(["foundry-cli", "--skip", "tests", "scripts"]);
185+
assert_eq!(args.skip, Some(vec![SkipBuildFilter::Tests, SkipBuildFilter::Scripts]));
186+
}
187+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Compiling 1 files with 0.8.17
2+
Solc 0.8.17 finished in 34.45ms
3+
Compiler run successful

cli/tests/it/cmd.rs

+20
Original file line numberDiff line numberDiff line change
@@ -1341,3 +1341,23 @@ forgetest_init!(can_install_missing_deps_build, |prj: TestProject, mut cmd: Test
13411341
assert!(output.contains("Missing dependencies found. Installing now"), "{}", output);
13421342
assert!(output.contains("Compiler run successful"), "{}", output);
13431343
});
1344+
1345+
// checks that extra output works
1346+
forgetest_init!(can_build_skip_contracts, |prj: TestProject, mut cmd: TestCommand| {
1347+
// explicitly set to run with 0.8.17 for consistent output
1348+
let config = Config { solc: Some("0.8.17".into()), ..Default::default() };
1349+
prj.write_config(config);
1350+
1351+
// only builds the single template contract `src/*`
1352+
cmd.args(["build", "--skip", "tests", "--skip", "scripts"]);
1353+
1354+
cmd.unchecked_output().stdout_matches_path(
1355+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1356+
.join("tests/fixtures/can_build_skip_contracts.stdout"),
1357+
);
1358+
// re-run command
1359+
let out = cmd.stdout();
1360+
1361+
// unchanged
1362+
assert!(out.trim().contains("No files changed, compilation skipped"), "{}", out);
1363+
});

0 commit comments

Comments
 (0)