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()) { 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    /// Check that the existing bindings match the expected abigen output
215    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    /// Generate the bindings
234    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}