forge/cmd/
bind.rs

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/// CLI arguments for `forge bind`.
20#[derive(Clone, Debug, Parser)]
21pub struct BindArgs {
22    /// Path to where the contract artifacts are stored.
23    #[arg(
24        long = "bindings-path",
25        short,
26        value_hint = ValueHint::DirPath,
27        value_name = "PATH"
28    )]
29    pub bindings: Option<PathBuf>,
30
31    /// Create bindings only for contracts whose names match the specified filter(s)
32    #[arg(long)]
33    pub select: Vec<regex::Regex>,
34
35    /// Explicitly generate bindings for all contracts
36    ///
37    /// By default all contracts ending with `Test` or `Script` are excluded.
38    #[arg(long, conflicts_with_all = &["select", "skip"])]
39    pub select_all: bool,
40
41    /// The name of the Rust crate to generate.
42    ///
43    /// This should be a valid crates.io crate name,
44    /// however, this is not currently validated by this command.
45    #[arg(long, default_value = DEFAULT_CRATE_NAME, value_name = "NAME")]
46    crate_name: String,
47
48    /// The version of the Rust crate to generate.
49    ///
50    /// This should be a standard semver version string,
51    /// however, this is not currently validated by this command.
52    #[arg(long, default_value = DEFAULT_CRATE_VERSION, value_name = "VERSION")]
53    crate_version: String,
54
55    /// The description of the Rust crate to generate.
56    ///
57    /// This will be added to the package.description field in Cargo.toml.
58    #[arg(long, default_value = "", value_name = "DESCRIPTION")]
59    crate_description: String,
60
61    /// The license of the Rust crate to generate.
62    ///
63    /// This will be added to the package.license field in Cargo.toml.
64    #[arg(long, value_name = "LICENSE", default_value = "")]
65    crate_license: String,
66
67    /// Generate the bindings as a module instead of a crate.
68    #[arg(long)]
69    module: bool,
70
71    /// Overwrite existing generated bindings.
72    ///
73    /// By default, the command will check that the bindings are correct, and then exit. If
74    /// --overwrite is passed, it will instead delete and overwrite the bindings.
75    #[arg(long)]
76    overwrite: bool,
77
78    /// Generate bindings as a single file.
79    #[arg(long)]
80    single_file: bool,
81
82    /// Skip Cargo.toml consistency checks.
83    #[arg(long)]
84    skip_cargo_toml: bool,
85
86    /// Skips running forge build before generating binding
87    #[arg(long)]
88    skip_build: bool,
89
90    /// Don't add any additional derives to generated bindings
91    #[arg(long)]
92    skip_extra_derives: bool,
93
94    /// Generate bindings for the `alloy` library, instead of `ethers`.
95    #[arg(long, hide = true)]
96    alloy: bool,
97
98    /// Specify the `alloy` version on Crates.
99    #[arg(long)]
100    alloy_version: Option<String>,
101
102    /// Specify the `alloy` revision on GitHub.
103    #[arg(long, conflicts_with = "alloy_version")]
104    alloy_rev: Option<String>,
105
106    /// Generate bindings for the `ethers` library (removed), instead of `alloy`.
107    #[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            // Select all json files
148            return Ok(Filter::All);
149        }
150        if !self.select.is_empty() {
151            // Return json files that match the select regex
152            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        // Exclude defaults
165        Ok(Filter::skip_default())
166    }
167
168    /// Returns an iterator over the JSON files and the contract name in the `artifacts` directory.
169    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                // Ignore the build info JSON.
174                if path.to_str()?.contains("build-info") {
175                    return None;
176                }
177
178                // Ignore the `target` directory in case the user has built the project.
179                if path.iter().any(|comp| comp == "target") {
180                    return None;
181                }
182
183                // We don't want `.metadata.json` files.
184                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                // Best effort identifier cleanup.
192                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    /// Check that the existing bindings match the expected abigen output
219    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    /// Generate the bindings
238    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}