1use alloy_primitives::map::HashSet;
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use forge_sol_macro_gen::{MultiSolMacroGen, SolMacroGen};
5use foundry_cli::{opts::BuildOpts, utils::LoadConfig};
6use foundry_common::{
7 compile::{ProjectCompiler, compile_abi_project},
8 fs::json_files,
9};
10use foundry_config::impl_figment_convert;
11use regex::Regex;
12use std::{
13 fs,
14 path::{Path, PathBuf},
15};
16
17impl_figment_convert!(BindArgs, build);
18
19const DEFAULT_CRATE_NAME: &str = "foundry-contracts";
20const DEFAULT_CRATE_VERSION: &str = "0.1.0";
21
22#[derive(Clone, Debug, Parser)]
24pub struct BindArgs {
25 #[arg(
27 long = "bindings-path",
28 short,
29 value_hint = ValueHint::DirPath,
30 value_name = "PATH"
31 )]
32 pub bindings: Option<PathBuf>,
33
34 #[arg(long)]
36 pub select: Vec<regex::Regex>,
37
38 #[arg(long, conflicts_with_all = &["select", "skip"])]
42 pub select_all: bool,
43
44 #[arg(long, default_value = DEFAULT_CRATE_NAME, value_name = "NAME")]
49 crate_name: String,
50
51 #[arg(long, default_value = DEFAULT_CRATE_VERSION, value_name = "VERSION")]
56 crate_version: String,
57
58 #[arg(long, default_value = "", value_name = "DESCRIPTION")]
62 crate_description: String,
63
64 #[arg(long, value_name = "LICENSE", default_value = "")]
68 crate_license: String,
69
70 #[arg(long)]
72 module: bool,
73
74 #[arg(long)]
79 overwrite: bool,
80
81 #[arg(long)]
83 single_file: bool,
84
85 #[arg(long)]
87 skip_cargo_toml: bool,
88
89 #[arg(long)]
91 skip_build: bool,
92
93 #[arg(long)]
95 skip_extra_derives: bool,
96
97 #[arg(long, hide = true)]
99 alloy: bool,
100
101 #[arg(long)]
103 alloy_version: Option<String>,
104
105 #[arg(long, conflicts_with = "alloy_version")]
107 alloy_rev: Option<String>,
108
109 #[arg(long, hide = true)]
111 ethers: bool,
112
113 #[command(flatten)]
114 build: BuildOpts,
115}
116
117impl BindArgs {
118 pub fn run(self) -> Result<()> {
119 if self.ethers {
120 eyre::bail!("`--ethers` bindings have been removed. Use `--alloy` (default) instead.");
121 }
122
123 if !self.skip_build {
124 let mut project = self.build.project()?;
125 let _ = compile_abi_project(&mut project, ProjectCompiler::new())?;
126 }
127
128 let config = self.load_config()?;
129 let artifacts = config.out;
130 let bindings_root = self.bindings.clone().unwrap_or_else(|| artifacts.join("bindings"));
131
132 if bindings_root.exists() {
133 if !self.overwrite {
134 sh_status!("Bindings found. Checking for consistency.")?;
135 return self.check_existing_bindings(&artifacts, &bindings_root);
136 }
137
138 trace!(?artifacts, "Removing existing bindings");
139 fs::remove_dir_all(&bindings_root)?;
140 }
141
142 self.generate_bindings(&artifacts, &bindings_root)?;
143
144 sh_status!("Bindings have been generated to {}", bindings_root.display())?;
145 Ok(())
146 }
147
148 fn get_filter(&self) -> Result<Filter> {
149 if self.select_all {
150 return Ok(Filter::All);
152 }
153 if !self.select.is_empty() {
154 return Ok(Filter::Select(self.select.clone()));
156 }
157
158 if let Some(skip) = self.build.skip.as_ref().filter(|s| !s.is_empty()) {
159 return Ok(Filter::Skip(
160 skip.clone()
161 .into_iter()
162 .map(|s| Regex::new(s.file_pattern()))
163 .collect::<Result<Vec<_>, _>>()?,
164 ));
165 }
166
167 Ok(Filter::skip_default())
169 }
170
171 fn get_json_files(&self, artifacts: &Path) -> Result<impl Iterator<Item = (String, PathBuf)>> {
173 let filter = self.get_filter()?;
174 Ok(json_files(artifacts)
175 .filter_map(|path| {
176 if path.to_str()?.contains("build-info") {
178 return None;
179 }
180
181 if path.iter().any(|comp| comp == "target") {
183 return None;
184 }
185
186 let stem = path.file_stem()?.to_str()?;
188 if stem.ends_with(".metadata") {
189 return None;
190 }
191
192 let name = stem.split('.').next().unwrap();
193
194 let name = name.replace(char::is_whitespace, "").replace('-', "_");
196
197 Some((name, path))
198 })
199 .filter(move |(name, _path)| filter.is_match(name)))
200 }
201
202 fn get_solmacrogen(&self, artifacts: &Path) -> Result<MultiSolMacroGen> {
203 let mut dup = HashSet::<String>::default();
204 let instances = self
205 .get_json_files(artifacts)?
206 .filter_map(|(name, path)| {
207 trace!(?path, "parsing SolMacroGen from file");
208 dup.insert(name.clone()).then(|| SolMacroGen::new(path, name))
209 })
210 .collect::<Vec<_>>();
211
212 let multi = MultiSolMacroGen::new(instances);
213 eyre::ensure!(!multi.instances.is_empty(), "No contract artifacts found");
214 Ok(multi)
215 }
216
217 fn check_existing_bindings(&self, artifacts: &Path, bindings_root: &Path) -> Result<()> {
219 let mut bindings = self.get_solmacrogen(artifacts)?;
220 bindings.generate_bindings(!self.skip_extra_derives)?;
221 sh_status!("Checking bindings for {} contracts", bindings.instances.len())?;
222 bindings.check_consistency(
223 &self.crate_name,
224 &self.crate_version,
225 bindings_root,
226 self.single_file,
227 !self.skip_cargo_toml,
228 self.module,
229 self.alloy_version.clone(),
230 self.alloy_rev.clone(),
231 )?;
232 sh_status!("OK.")?;
233 Ok(())
234 }
235
236 fn generate_bindings(&self, artifacts: &Path, bindings_root: &Path) -> Result<()> {
238 let mut solmacrogen = self.get_solmacrogen(artifacts)?;
239 sh_status!("Generating bindings for {} contracts", solmacrogen.instances.len())?;
240
241 if self.module {
242 trace!(single_file = self.single_file, "generating module");
243 solmacrogen.write_to_module(
244 bindings_root,
245 self.single_file,
246 !self.skip_extra_derives,
247 )?;
248 } else {
249 trace!(single_file = self.single_file, "generating crate");
250 solmacrogen.write_to_crate(
251 &self.crate_name,
252 &self.crate_version,
253 &self.crate_description,
254 &self.crate_license,
255 bindings_root,
256 self.single_file,
257 self.alloy_version.clone(),
258 self.alloy_rev.clone(),
259 !self.skip_extra_derives,
260 )?;
261 }
262
263 Ok(())
264 }
265}
266
267pub enum Filter {
268 All,
269 Select(Vec<regex::Regex>),
270 Skip(Vec<regex::Regex>),
271}
272
273impl Filter {
274 pub fn is_match(&self, name: &str) -> bool {
275 match self {
276 Self::All => true,
277 Self::Select(regexes) => regexes.iter().any(|regex| regex.is_match(name)),
278 Self::Skip(regexes) => !regexes.iter().any(|regex| regex.is_match(name)),
279 }
280 }
281
282 pub fn skip_default() -> Self {
283 let skip = [
284 ".*Test.*",
285 ".*Script",
286 "console[2]?",
287 "CommonBase",
288 "Components",
289 "[Ss]td(Chains|Math|Error|Json|Utils|Cheats|Style|Invariant|Assertions|Toml|Storage(Safe)?)",
290 "[Vv]m.*",
291 "IMulticall3",
292 ]
293 .iter()
294 .map(|pattern| regex::Regex::new(pattern).unwrap())
295 .collect::<Vec<_>>();
296
297 Self::Skip(skip)
298 }
299}