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