Skip to main content

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::{
7    compile::{ProjectCompiler, compile_abi_project},
8    fs::json_files,
9};
10use foundry_config::impl_figment_convert;
11use regex::Regex;
12use std::{
13    fs,
14    path::{Path, PathBuf},
15};
16
17impl_figment_convert!(BindArgs, build);
18
19const DEFAULT_CRATE_NAME: &str = "foundry-contracts";
20const DEFAULT_CRATE_VERSION: &str = "0.1.0";
21
22/// CLI arguments for `forge bind`.
23#[derive(Clone, Debug, Parser)]
24pub struct BindArgs {
25    /// Path to where the contract artifacts are stored.
26    #[arg(
27        long = "bindings-path",
28        short,
29        value_hint = ValueHint::DirPath,
30        value_name = "PATH"
31    )]
32    pub bindings: Option<PathBuf>,
33
34    /// Create bindings only for contracts whose names match the specified filter(s)
35    #[arg(long)]
36    pub select: Vec<regex::Regex>,
37
38    /// Explicitly generate bindings for all contracts
39    ///
40    /// By default all contracts ending with `Test` or `Script` are excluded.
41    #[arg(long, conflicts_with_all = &["select", "skip"])]
42    pub select_all: bool,
43
44    /// The name of the Rust crate to generate.
45    ///
46    /// This should be a valid crates.io crate name,
47    /// however, this is not currently validated by this command.
48    #[arg(long, default_value = DEFAULT_CRATE_NAME, value_name = "NAME")]
49    crate_name: String,
50
51    /// The version of the Rust crate to generate.
52    ///
53    /// This should be a standard semver version string,
54    /// however, this is not currently validated by this command.
55    #[arg(long, default_value = DEFAULT_CRATE_VERSION, value_name = "VERSION")]
56    crate_version: String,
57
58    /// The description of the Rust crate to generate.
59    ///
60    /// This will be added to the package.description field in Cargo.toml.
61    #[arg(long, default_value = "", value_name = "DESCRIPTION")]
62    crate_description: String,
63
64    /// The license of the Rust crate to generate.
65    ///
66    /// This will be added to the package.license field in Cargo.toml.
67    #[arg(long, value_name = "LICENSE", default_value = "")]
68    crate_license: String,
69
70    /// Generate the bindings as a module instead of a crate.
71    #[arg(long)]
72    module: bool,
73
74    /// Overwrite existing generated bindings.
75    ///
76    /// By default, the command will check that the bindings are correct, and then exit. If
77    /// --overwrite is passed, it will instead delete and overwrite the bindings.
78    #[arg(long)]
79    overwrite: bool,
80
81    /// Generate bindings as a single file.
82    #[arg(long)]
83    single_file: bool,
84
85    /// Skip Cargo.toml consistency checks.
86    #[arg(long)]
87    skip_cargo_toml: bool,
88
89    /// Skips running forge build before generating binding
90    #[arg(long)]
91    skip_build: bool,
92
93    /// Don't add any additional derives to generated bindings
94    #[arg(long)]
95    skip_extra_derives: bool,
96
97    /// Generate bindings for the `alloy` library, instead of `ethers`.
98    #[arg(long, hide = true)]
99    alloy: bool,
100
101    /// Specify the `alloy` version on Crates.
102    #[arg(long)]
103    alloy_version: Option<String>,
104
105    /// Specify the `alloy` revision on GitHub.
106    #[arg(long, conflicts_with = "alloy_version")]
107    alloy_rev: Option<String>,
108
109    /// Generate bindings for the `ethers` library (removed), instead of `alloy`.
110    #[arg(long, hide = true)]
111    ethers: bool,
112
113    #[command(flatten)]
114    build: BuildOpts,
115}
116
117impl BindArgs {
118    pub fn run(self) -> Result<()> {
119        if self.ethers {
120            eyre::bail!("`--ethers` bindings have been removed. Use `--alloy` (default) instead.");
121        }
122
123        if !self.skip_build {
124            let mut project = self.build.project()?;
125            let _ = compile_abi_project(&mut project, ProjectCompiler::new())?;
126        }
127
128        let config = self.load_config()?;
129        let artifacts = config.out;
130        let bindings_root = self.bindings.clone().unwrap_or_else(|| artifacts.join("bindings"));
131
132        if bindings_root.exists() {
133            if !self.overwrite {
134                sh_status!("Bindings found. Checking for consistency.")?;
135                return self.check_existing_bindings(&artifacts, &bindings_root);
136            }
137
138            trace!(?artifacts, "Removing existing bindings");
139            fs::remove_dir_all(&bindings_root)?;
140        }
141
142        self.generate_bindings(&artifacts, &bindings_root)?;
143
144        sh_status!("Bindings have been generated to {}", bindings_root.display())?;
145        Ok(())
146    }
147
148    fn get_filter(&self) -> Result<Filter> {
149        if self.select_all {
150            // Select all json files
151            return Ok(Filter::All);
152        }
153        if !self.select.is_empty() {
154            // Return json files that match the select regex
155            return Ok(Filter::Select(self.select.clone()));
156        }
157
158        if let Some(skip) = self.build.skip.as_ref().filter(|s| !s.is_empty()) {
159            return Ok(Filter::Skip(
160                skip.clone()
161                    .into_iter()
162                    .map(|s| Regex::new(s.file_pattern()))
163                    .collect::<Result<Vec<_>, _>>()?,
164            ));
165        }
166
167        // Exclude defaults
168        Ok(Filter::skip_default())
169    }
170
171    /// Returns an iterator over the JSON files and the contract name in the `artifacts` directory.
172    fn get_json_files(&self, artifacts: &Path) -> Result<impl Iterator<Item = (String, PathBuf)>> {
173        let filter = self.get_filter()?;
174        Ok(json_files(artifacts)
175            .filter_map(|path| {
176                // Ignore the build info JSON.
177                if path.to_str()?.contains("build-info") {
178                    return None;
179                }
180
181                // Ignore the `target` directory in case the user has built the project.
182                if path.iter().any(|comp| comp == "target") {
183                    return None;
184                }
185
186                // We don't want `.metadata.json` files.
187                let stem = path.file_stem()?.to_str()?;
188                if stem.ends_with(".metadata") {
189                    return None;
190                }
191
192                let name = stem.split('.').next().unwrap();
193
194                // Best effort identifier cleanup.
195                let name = name.replace(char::is_whitespace, "").replace('-', "_");
196
197                Some((name, path))
198            })
199            .filter(move |(name, _path)| filter.is_match(name)))
200    }
201
202    fn get_solmacrogen(&self, artifacts: &Path) -> Result<MultiSolMacroGen> {
203        let mut dup = HashSet::<String>::default();
204        let instances = self
205            .get_json_files(artifacts)?
206            .filter_map(|(name, path)| {
207                trace!(?path, "parsing SolMacroGen from file");
208                dup.insert(name.clone()).then(|| SolMacroGen::new(path, name))
209            })
210            .collect::<Vec<_>>();
211
212        let multi = MultiSolMacroGen::new(instances);
213        eyre::ensure!(!multi.instances.is_empty(), "No contract artifacts found");
214        Ok(multi)
215    }
216
217    /// Check that the existing bindings match the expected abigen output
218    fn check_existing_bindings(&self, artifacts: &Path, bindings_root: &Path) -> Result<()> {
219        let mut bindings = self.get_solmacrogen(artifacts)?;
220        bindings.generate_bindings(!self.skip_extra_derives)?;
221        sh_status!("Checking bindings for {} contracts", bindings.instances.len())?;
222        bindings.check_consistency(
223            &self.crate_name,
224            &self.crate_version,
225            bindings_root,
226            self.single_file,
227            !self.skip_cargo_toml,
228            self.module,
229            self.alloy_version.clone(),
230            self.alloy_rev.clone(),
231        )?;
232        sh_status!("OK.")?;
233        Ok(())
234    }
235
236    /// Generate the bindings
237    fn generate_bindings(&self, artifacts: &Path, bindings_root: &Path) -> Result<()> {
238        let mut solmacrogen = self.get_solmacrogen(artifacts)?;
239        sh_status!("Generating bindings for {} contracts", solmacrogen.instances.len())?;
240
241        if self.module {
242            trace!(single_file = self.single_file, "generating module");
243            solmacrogen.write_to_module(
244                bindings_root,
245                self.single_file,
246                !self.skip_extra_derives,
247            )?;
248        } else {
249            trace!(single_file = self.single_file, "generating crate");
250            solmacrogen.write_to_crate(
251                &self.crate_name,
252                &self.crate_version,
253                &self.crate_description,
254                &self.crate_license,
255                bindings_root,
256                self.single_file,
257                self.alloy_version.clone(),
258                self.alloy_rev.clone(),
259                !self.skip_extra_derives,
260            )?;
261        }
262
263        Ok(())
264    }
265}
266
267pub enum Filter {
268    All,
269    Select(Vec<regex::Regex>),
270    Skip(Vec<regex::Regex>),
271}
272
273impl Filter {
274    pub fn is_match(&self, name: &str) -> bool {
275        match self {
276            Self::All => true,
277            Self::Select(regexes) => regexes.iter().any(|regex| regex.is_match(name)),
278            Self::Skip(regexes) => !regexes.iter().any(|regex| regex.is_match(name)),
279        }
280    }
281
282    pub fn skip_default() -> Self {
283        let skip = [
284            ".*Test.*",
285            ".*Script",
286            "console[2]?",
287            "CommonBase",
288            "Components",
289            "[Ss]td(Chains|Math|Error|Json|Utils|Cheats|Style|Invariant|Assertions|Toml|Storage(Safe)?)",
290            "[Vv]m.*",
291            "IMulticall3",
292        ]
293        .iter()
294        .map(|pattern| regex::Regex::new(pattern).unwrap())
295        .collect::<Vec<_>>();
296
297        Self::Skip(skip)
298    }
299}