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()) { Some(SolMacroGen::new(path, name)) } else { None }
206 })
207 .collect::<Vec<_>>();
208
209 let multi = MultiSolMacroGen::new(artifacts, instances);
210 eyre::ensure!(!multi.instances.is_empty(), "No contract artifacts found");
211 Ok(multi)
212 }
213
214 fn check_existing_bindings(&self, artifacts: &Path, bindings_root: &Path) -> Result<()> {
216 let mut bindings = self.get_solmacrogen(artifacts)?;
217 bindings.generate_bindings(!self.skip_extra_derives)?;
218 sh_println!("Checking bindings for {} contracts", bindings.instances.len())?;
219 bindings.check_consistency(
220 &self.crate_name,
221 &self.crate_version,
222 bindings_root,
223 self.single_file,
224 !self.skip_cargo_toml,
225 self.module,
226 self.alloy_version.clone(),
227 self.alloy_rev.clone(),
228 )?;
229 sh_println!("OK.")?;
230 Ok(())
231 }
232
233 fn generate_bindings(&self, artifacts: &Path, bindings_root: &Path) -> Result<()> {
235 let mut solmacrogen = self.get_solmacrogen(artifacts)?;
236 sh_println!("Generating bindings for {} contracts", solmacrogen.instances.len())?;
237
238 if !self.module {
239 trace!(single_file = self.single_file, "generating crate");
240 solmacrogen.write_to_crate(
241 &self.crate_name,
242 &self.crate_version,
243 &self.crate_description,
244 &self.crate_license,
245 bindings_root,
246 self.single_file,
247 self.alloy_version.clone(),
248 self.alloy_rev.clone(),
249 !self.skip_extra_derives,
250 )?;
251 } else {
252 trace!(single_file = self.single_file, "generating module");
253 solmacrogen.write_to_module(
254 bindings_root,
255 self.single_file,
256 !self.skip_extra_derives,
257 )?;
258 }
259
260 Ok(())
261 }
262}
263
264pub enum Filter {
265 All,
266 Select(Vec<regex::Regex>),
267 Skip(Vec<regex::Regex>),
268}
269
270impl Filter {
271 pub fn is_match(&self, name: &str) -> bool {
272 match self {
273 Self::All => true,
274 Self::Select(regexes) => regexes.iter().any(|regex| regex.is_match(name)),
275 Self::Skip(regexes) => !regexes.iter().any(|regex| regex.is_match(name)),
276 }
277 }
278
279 pub fn skip_default() -> Self {
280 let skip = [
281 ".*Test.*",
282 ".*Script",
283 "console[2]?",
284 "CommonBase",
285 "Components",
286 "[Ss]td(Chains|Math|Error|Json|Utils|Cheats|Style|Invariant|Assertions|Toml|Storage(Safe)?)",
287 "[Vv]m.*",
288 "IMulticall3",
289 ]
290 .iter()
291 .map(|pattern| regex::Regex::new(pattern).unwrap())
292 .collect::<Vec<_>>();
293
294 Self::Skip(skip)
295 }
296}