forge/cmd/
build.rs

1use super::{install, watch::WatchArgs};
2use clap::Parser;
3use eyre::Result;
4use forge_lint::{linter::Linter, sol::SolidityLinter};
5use foundry_cli::{
6    opts::BuildOpts,
7    utils::{cache_local_signatures, LoadConfig},
8};
9use foundry_common::{compile::ProjectCompiler, shell};
10use foundry_compilers::{
11    compilers::{multi::MultiCompilerLanguage, Language},
12    solc::SolcLanguage,
13    utils::source_files_iter,
14    Project, ProjectCompileOutput,
15};
16use foundry_config::{
17    figment::{
18        self,
19        error::Kind::InvalidType,
20        value::{Dict, Map, Value},
21        Metadata, Profile, Provider,
22    },
23    filter::expand_globs,
24    Config,
25};
26use serde::Serialize;
27use std::path::PathBuf;
28
29foundry_config::merge_impl_figment_convert!(BuildArgs, build);
30
31/// CLI arguments for `forge build`.
32///
33/// CLI arguments take the highest precedence in the Config/Figment hierarchy.
34/// In order to override them in the foundry `Config` they need to be merged into an existing
35/// `figment::Provider`, like `foundry_config::Config` is.
36///
37/// `BuildArgs` implements `figment::Provider` in which all config related fields are serialized and
38/// then merged into an existing `Config`, effectively overwriting them.
39///
40/// Some arguments are marked as `#[serde(skip)]` and require manual processing in
41/// `figment::Provider` implementation
42#[derive(Clone, Debug, Default, Serialize, Parser)]
43#[command(next_help_heading = "Build options", about = None, long_about = None)] // override doc
44pub struct BuildArgs {
45    /// Build source files from specified paths.
46    #[serde(skip)]
47    pub paths: Option<Vec<PathBuf>>,
48
49    /// Print compiled contract names.
50    #[arg(long)]
51    #[serde(skip)]
52    pub names: bool,
53
54    /// Print compiled contract sizes.
55    /// Constructor argument length is not included in the calculation of initcode size.
56    #[arg(long)]
57    #[serde(skip)]
58    pub sizes: bool,
59
60    /// Ignore initcode contract bytecode size limit introduced by EIP-3860.
61    #[arg(long, alias = "ignore-initcode-size")]
62    #[serde(skip)]
63    pub ignore_eip_3860: bool,
64
65    #[command(flatten)]
66    #[serde(flatten)]
67    pub build: BuildOpts,
68
69    #[command(flatten)]
70    #[serde(skip)]
71    pub watch: WatchArgs,
72}
73
74impl BuildArgs {
75    pub fn run(self) -> Result<ProjectCompileOutput> {
76        let mut config = self.load_config()?;
77
78        if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings {
79            // need to re-configure here to also catch additional remappings
80            config = self.load_config()?;
81        }
82
83        let project = config.project()?;
84
85        // Collect sources to compile if build subdirectories specified.
86        let mut files = vec![];
87        if let Some(paths) = &self.paths {
88            for path in paths {
89                let joined = project.root().join(path);
90                let path = if joined.exists() { &joined } else { path };
91                files.extend(source_files_iter(path, MultiCompilerLanguage::FILE_EXTENSIONS));
92            }
93            if files.is_empty() {
94                eyre::bail!("No source files found in specified build paths.")
95            }
96        }
97
98        let format_json = shell::is_json();
99        let compiler = ProjectCompiler::new()
100            .files(files)
101            .dynamic_test_linking(config.dynamic_test_linking)
102            .print_names(self.names)
103            .print_sizes(self.sizes)
104            .ignore_eip_3860(self.ignore_eip_3860)
105            .bail(!format_json);
106
107        // Runs the SolidityLinter before compilation.
108        self.lint(&project, &config)?;
109        let output = compiler.compile(&project)?;
110
111        // Cache project selectors.
112        cache_local_signatures(&output)?;
113
114        if format_json && !self.names && !self.sizes {
115            sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
116        }
117
118        Ok(output)
119    }
120
121    fn lint(&self, project: &Project, config: &Config) -> Result<()> {
122        let format_json = shell::is_json();
123        if project.compiler.solc.is_some() && config.lint.lint_on_build && !shell::is_quiet() {
124            let linter = SolidityLinter::new(config.project_paths())
125                .with_json_emitter(format_json)
126                .with_description(!format_json)
127                .with_severity(if config.lint.severity.is_empty() {
128                    None
129                } else {
130                    Some(config.lint.severity.clone())
131                })
132                .without_lints(if config.lint.exclude_lints.is_empty() {
133                    None
134                } else {
135                    Some(
136                        config
137                            .lint
138                            .exclude_lints
139                            .iter()
140                            .filter_map(|s| forge_lint::sol::SolLint::try_from(s.as_str()).ok())
141                            .collect(),
142                    )
143                });
144
145            // Expand ignore globs and canonicalize from the get go
146            let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
147                .iter()
148                .flat_map(foundry_common::fs::canonicalize_path)
149                .collect::<Vec<_>>();
150
151            let curr_dir = std::env::current_dir()?;
152            let input_files = config
153                .project_paths::<SolcLanguage>()
154                .input_files_iter()
155                .filter(|p| !(ignored.contains(p) || ignored.contains(&curr_dir.join(p))))
156                .collect::<Vec<_>>();
157
158            if !input_files.is_empty() {
159                linter.lint(&input_files);
160            }
161        }
162
163        Ok(())
164    }
165
166    /// Returns the `Project` for the current workspace
167    ///
168    /// This loads the `foundry_config::Config` for the current workspace (see
169    /// [`foundry_config::utils::find_project_root`] and merges the cli `BuildArgs` into it before
170    /// returning [`foundry_config::Config::project()`]
171    pub fn project(&self) -> Result<Project> {
172        self.build.project()
173    }
174
175    /// Returns whether `BuildArgs` was configured with `--watch`
176    pub fn is_watch(&self) -> bool {
177        self.watch.watch.is_some()
178    }
179
180    /// Returns the [`watchexec::Config`] necessary to bootstrap a new watch loop.
181    pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
182        // Use the path arguments or if none where provided the `src`, `test` and `script`
183        // directories as well as the `foundry.toml` configuration file.
184        self.watch.watchexec_config(|| {
185            let config = self.load_config()?;
186            let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
187            Ok([config.src, config.test, config.script, foundry_toml])
188        })
189    }
190}
191
192// Make this args a `figment::Provider` so that it can be merged into the `Config`
193impl Provider for BuildArgs {
194    fn metadata(&self) -> Metadata {
195        Metadata::named("Build Args Provider")
196    }
197
198    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
199        let value = Value::serialize(self)?;
200        let error = InvalidType(value.to_actual(), "map".into());
201        let mut dict = value.into_dict().ok_or(error)?;
202
203        if self.names {
204            dict.insert("names".to_string(), true.into());
205        }
206
207        if self.sizes {
208            dict.insert("sizes".to_string(), true.into());
209        }
210
211        if self.ignore_eip_3860 {
212            dict.insert("ignore_eip_3860".to_string(), true.into());
213        }
214
215        Ok(Map::from([(Config::selected_profile(), dict)]))
216    }
217}