Skip to main content

forge/cmd/
build.rs

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