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