1use super::{install, watch::WatchArgs};
2use clap::Parser;
3use eyre::{Context, Result};
4use forge_lint::{linter::Linter, sol::SolidityLinter};
5use foundry_cli::{
6 opts::{BuildOpts, configure_pcx_from_solc, get_solar_sources_from_compile_output},
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#[derive(Clone, Debug, Default, Serialize, Parser)]
42#[command(next_help_heading = "Build options", about = None, long_about = None)] pub struct BuildArgs {
44 #[serde(skip)]
46 pub paths: Option<Vec<PathBuf>>,
47
48 #[arg(long)]
50 #[serde(skip)]
51 pub names: bool,
52
53 #[arg(long)]
56 #[serde(skip)]
57 pub sizes: bool,
58
59 #[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 async fn run(self) -> Result<ProjectCompileOutput> {
75 let mut config = self.load_config()?;
76
77 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
78 {
79 config = self.load_config()?;
81 }
82
83 let project = config.project()?;
84
85 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 let mut output = compiler.compile(&project)?;
108
109 cache_local_signatures(&output)?;
111
112 if format_json && !self.names && !self.sizes {
113 sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
114 }
115
116 if config.lint.lint_on_build && !output.output().errors.iter().any(|e| e.is_error()) {
118 self.lint(&project, &config, self.paths.as_deref(), &mut output)
119 .wrap_err("Lint failed")?;
120 }
121
122 Ok(output)
123 }
124
125 fn lint(
126 &self,
127 project: &Project,
128 config: &Config,
129 files: Option<&[PathBuf]>,
130 output: &mut ProjectCompileOutput,
131 ) -> Result<()> {
132 let format_json = shell::is_json();
133 if project.compiler.solc.is_some() && !shell::is_quiet() {
134 let linter = SolidityLinter::new(config.project_paths())
135 .with_json_emitter(format_json)
136 .with_description(!format_json)
137 .with_severity(if config.lint.severity.is_empty() {
138 None
139 } else {
140 Some(config.lint.severity.clone())
141 })
142 .without_lints(if config.lint.exclude_lints.is_empty() {
143 None
144 } else {
145 Some(
146 config
147 .lint
148 .exclude_lints
149 .iter()
150 .filter_map(|s| forge_lint::sol::SolLint::try_from(s.as_str()).ok())
151 .collect(),
152 )
153 })
154 .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions);
155
156 let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
158 .iter()
159 .flat_map(foundry_common::fs::canonicalize_path)
160 .collect::<Vec<_>>();
161
162 let skip = SkipBuildFilters::new(config.skip.clone(), config.root.clone());
163 let curr_dir = std::env::current_dir()?;
164 let input_files = config
165 .project_paths::<SolcLanguage>()
166 .input_files_iter()
167 .filter(|p| {
168 if let Some(files) = files {
170 return files.iter().any(|file| &curr_dir.join(file) == p);
171 }
172 skip.is_match(p)
173 && !(ignored.contains(p) || ignored.contains(&curr_dir.join(p)))
174 })
175 .collect::<Vec<_>>();
176
177 let solar_sources =
178 get_solar_sources_from_compile_output(config, output, Some(&input_files))?;
179 if solar_sources.input.sources.is_empty() {
180 if !input_files.is_empty() {
181 sh_warn!(
182 "unable to lint. Solar only supports Solidity versions prior to 0.8.0"
183 )?;
184 }
185 return Ok(());
186 }
187
188 let mut compiler = solar::sema::Compiler::new(
191 solar::interface::Session::builder().with_stderr_emitter().build(),
192 );
193
194 compiler.enter_mut(|compiler| {
196 let mut pcx = compiler.parse();
197 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
198 pcx.set_resolve_imports(true);
199 pcx.parse();
200 });
201 linter.lint(&input_files, config.deny, &mut compiler)?;
202 }
203
204 Ok(())
205 }
206
207 pub fn project(&self) -> Result<Project> {
213 self.build.project()
214 }
215
216 pub fn is_watch(&self) -> bool {
218 self.watch.watch.is_some()
219 }
220
221 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
223 self.watch.watchexec_config(|| {
226 let config = self.load_config()?;
227 let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
228 Ok([config.src, config.test, config.script, foundry_toml])
229 })
230 }
231}
232
233impl Provider for BuildArgs {
235 fn metadata(&self) -> Metadata {
236 Metadata::named("Build Args Provider")
237 }
238
239 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
240 let value = Value::serialize(self)?;
241 let error = InvalidType(value.to_actual(), "map".into());
242 let mut dict = value.into_dict().ok_or(error)?;
243
244 if self.names {
245 dict.insert("names".to_string(), true.into());
246 }
247
248 if self.sizes {
249 dict.insert("sizes".to_string(), true.into());
250 }
251
252 if self.ignore_eip_3860 {
253 dict.insert("ignore_eip_3860".to_string(), true.into());
254 }
255
256 Ok(Map::from([(Config::selected_profile(), dict)]))
257 }
258}