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