1use super::watch::WatchArgs;
2use crate::Lockfile;
3use clap::Parser;
4use eyre::Result;
5use forge_lint::{
6 linter::Linter,
7 sol::{DeniedLintDiagnostics, SolidityLinter},
8};
9use foundry_cli::{
10 opts::{BuildOpts, configure_pcx_from_solc, get_solar_sources_from_compile_output},
11 utils::{Git, LoadConfig, cache_local_signatures},
12};
13use foundry_common::{
14 compile::{ContractSizeLimits, ProjectCompiler},
15 shell,
16};
17use foundry_compilers::{
18 CompilationError, FileFilter, Project, ProjectCompileOutput,
19 compilers::{Language, multi::MultiCompilerLanguage},
20 solc::SolcLanguage,
21 utils::source_files_iter,
22};
23use foundry_config::{
24 Config, SkipBuildFilters,
25 figment::{
26 self, Metadata, Profile, Provider,
27 error::Kind::InvalidType,
28 value::{Dict, Map, Value},
29 },
30 filter::{expand_globs, is_ignored_path},
31};
32use serde::Serialize;
33use solar::{
34 interface::{Session, config::CompileOpts},
35 sema::Compiler,
36};
37use std::{fmt::Write, path::PathBuf};
38
39foundry_config::merge_impl_figment_convert!(BuildArgs, build);
40
41#[derive(Clone, Debug, Default, Serialize, Parser)]
53#[command(next_help_heading = "Build options", about = None, long_about = None)] pub struct BuildArgs {
55 #[serde(skip)]
57 pub paths: Option<Vec<PathBuf>>,
58
59 #[arg(long)]
61 #[serde(skip)]
62 pub names: bool,
63
64 #[arg(long)]
67 #[serde(skip)]
68 pub sizes: bool,
69
70 #[arg(long, alias = "ignore-initcode-size")]
72 #[serde(skip)]
73 pub ignore_eip_3860: bool,
74
75 #[arg(long, visible_alias = "skip-lint")]
80 #[serde(skip)]
81 pub no_lint: bool,
82
83 #[command(flatten)]
84 #[serde(flatten)]
85 pub build: BuildOpts,
86
87 #[command(flatten)]
88 #[serde(skip)]
89 pub watch: WatchArgs,
90}
91
92impl BuildArgs {
93 pub async fn run(self, locked: bool) -> Result<ProjectCompileOutput> {
94 let mut config = self.load_config()?;
95
96 if locked {
97 self.check_foundry_lock_consistency(&config)?;
98 }
99
100 self.install_missing_dependencies(&mut config)?;
101
102 self.check_soldeer_lock_consistency(&config).await;
103
104 let project = config.project()?;
105
106 let mut files = vec![];
108 let mut selected_paths = vec![];
109 if let Some(paths) = &self.paths {
110 for path in paths {
111 let joined = project.root().join(path);
112 let path = if joined.exists() { &joined } else { path };
113 selected_paths.push(path.clone());
114 files.extend(source_files_iter(path, MultiCompilerLanguage::FILE_EXTENSIONS));
115 }
116 if files.is_empty() && config.external_compilers.is_empty() {
117 eyre::bail!("No source files found in specified build paths.");
118 }
119 }
120
121 let format_json = shell::is_json();
122
123 let mut output = ProjectCompiler::new()
124 .external_compilers(&config)
125 .files(files)
126 .selected_paths(selected_paths)
127 .dynamic_test_linking(config.dynamic_test_linking)
128 .print_compiler_settings(shell::verbosity() >= 2)
129 .print_names(self.names)
130 .print_sizes(self.sizes)
131 .ignore_eip_3860(self.ignore_eip_3860)
132 .size_limits(contract_size_limits(&config))
133 .bail(!format_json)
134 .compile(&project)?;
135
136 cache_local_signatures(&output)?;
138
139 if format_json && (!self.names && !self.sizes || output.has_compiler_errors()) {
140 sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
141 }
142 if format_json && output.has_compiler_errors() {
143 std::process::exit(1);
144 }
145
146 if !self.no_lint
148 && config.lint.lint_on_build
149 && !output.output().errors.iter().any(|e| e.is_error())
150 && let Err(err) = self.lint(&project, &config, self.paths.as_deref(), &mut output)
151 {
152 if err.downcast_ref::<DeniedLintDiagnostics>().is_none() {
153 emit_lint_failure_notice();
154 }
155 return Err(err.wrap_err("post-build lint step failed"));
156 }
157
158 Ok(output)
159 }
160
161 fn lint(
162 &self,
163 project: &Project,
164 config: &Config,
165 files: Option<&[PathBuf]>,
166 output: &mut ProjectCompileOutput,
167 ) -> Result<()> {
168 let format_json = shell::is_json();
169 if project.compiler.solc.is_some() && !shell::is_quiet() {
170 let linter = SolidityLinter::new(config.project_paths())
171 .with_json_emitter(format_json)
172 .with_description(!format_json)
173 .with_severity(if config.lint.severity.is_empty() {
174 None
175 } else {
176 Some(config.lint.severity.clone())
177 })
178 .without_lints(if config.lint.exclude_lints.is_empty() {
179 None
180 } else {
181 Some(
182 config
183 .lint
184 .exclude_lints
185 .iter()
186 .filter_map(|s| s.parse::<forge_lint::sol::SolLint>().ok())
187 .collect(),
188 )
189 })
190 .with_lint_specific(&config.lint.lint_specific);
191
192 let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
194 .iter()
195 .flat_map(foundry_common::fs::canonicalize_path)
196 .collect::<Vec<_>>();
197
198 let skip = SkipBuildFilters::new(config.skip.clone(), config.root.clone());
199 let curr_dir = std::env::current_dir()?;
200 let input_files = config
201 .project_paths::<SolcLanguage>()
202 .input_files_iter()
203 .filter(|p| {
204 if let Some(files) = files {
206 return files.iter().any(|file| &curr_dir.join(file) == p);
207 }
208 skip.is_match(p) && !is_ignored_path(p, &ignored, &curr_dir)
209 })
210 .collect::<Vec<_>>();
211
212 let solar_sources =
213 get_solar_sources_from_compile_output(config, output, Some(&input_files), None)?;
214 if solar_sources.input.sources.is_empty() {
215 if !input_files.is_empty() {
216 sh_warn!("unable to lint. Solar only supports Solidity versions >=0.8.0")?;
217 }
218 return Ok(());
219 }
220
221 let opts = CompileOpts::default();
224 let mut compiler =
225 Compiler::new(Session::builder().opts(opts).with_stderr_emitter().build());
226
227 compiler.enter_mut(|compiler| {
229 let mut pcx = compiler.parse();
230 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
231 pcx.set_resolve_imports(true);
232 pcx.parse();
233 });
234
235 linter.lint(&input_files, config.deny, &mut compiler)?;
236 }
237
238 Ok(())
239 }
240
241 pub fn project(&self) -> Result<Project> {
247 self.build.project()
248 }
249
250 pub const fn is_watch(&self) -> bool {
252 self.watch.watch.is_some()
253 }
254
255 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
257 self.watch.watchexec_config(|| {
260 let config = self.load_config()?;
261 let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
262 Ok([config.src, config.test, config.script, foundry_toml])
263 })
264 }
265
266 async fn check_soldeer_lock_consistency(&self, config: &Config) {
268 let soldeer_lock_path = config.root.join("soldeer.lock");
269 if !soldeer_lock_path.exists() {
270 return;
271 }
272
273 let Ok(lockfile) = soldeer_core::lock::read_lockfile(&soldeer_lock_path) else {
275 return;
276 };
277
278 let deps_dir = config.root.join("dependencies");
279 for entry in &lockfile.entries {
280 let dep_name = entry.name();
281
282 match soldeer_core::install::check_dependency_integrity(entry, &deps_dir).await {
284 Ok(status) => {
285 use soldeer_core::install::DependencyStatus;
286 if matches!(
288 status,
289 DependencyStatus::Missing | DependencyStatus::FailedIntegrity
290 ) {
291 sh_warn!("Dependency '{}' integrity check failed: {:?}", dep_name, status)
292 .ok();
293 }
294 }
295 Err(e) => {
296 sh_warn!("Dependency '{}' integrity check error: {}", dep_name, e).ok();
297 }
298 }
299 }
300 }
301
302 fn check_foundry_lock_consistency(&self, config: &Config) -> Result<()> {
304 let git = Git::new(&config.root);
305 let mut lockfile = Lockfile::new(&config.root).with_git(&git);
306 let mismatches = lockfile.check()?;
307 if mismatches.is_empty() {
308 return Ok(());
309 }
310
311 let mut message = String::from("foundry.lock does not match installed dependencies:");
312 for mismatch in mismatches {
313 write!(message, "\n {mismatch}")?;
314 }
315 Err(eyre::eyre!(message))
316 }
317}
318
319fn contract_size_limits(config: &Config) -> ContractSizeLimits {
320 config
321 .code_size_limit
322 .map(ContractSizeLimits::with_runtime_limit)
323 .or_else(|| {
324 config
325 .networks
326 .contract_size_limits()
327 .map(|limits| ContractSizeLimits::new(limits.runtime, limits.initcode))
328 })
329 .unwrap_or_else(|| ContractSizeLimits::for_spec_id(config.evm_spec_id()))
330}
331const LINT_FAILURE_NOTICE: &str = "\
334note: internal lint engine failure (compilation itself succeeded).
335note: please file a bug report at
336 https://github.com/foundry-rs/foundry/issues/new?template=BUG-FORM.yml
337 and attach the full output above.
338help: rerun with `--no-lint` to skip linting for this build, or consider temporarily
339 disabling forge lint on build:
340 https://getfoundry.sh/forge/linting#disable-linting-on-build
341";
342
343fn emit_lint_failure_notice() {
344 if shell::is_json() {
345 return;
346 }
347 let _ = sh_eprintln!("\n{LINT_FAILURE_NOTICE}");
348}
349
350impl Provider for BuildArgs {
352 fn metadata(&self) -> Metadata {
353 Metadata::named("Build Args Provider")
354 }
355
356 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
357 let value = Value::serialize(self)?;
358 let error = InvalidType(value.to_actual(), "map".into());
359 let mut dict = value.into_dict().ok_or(error)?;
360
361 if self.names {
362 dict.insert("names".to_string(), true.into());
363 }
364
365 if self.sizes {
366 dict.insert("sizes".to_string(), true.into());
367 }
368
369 if self.ignore_eip_3860 {
370 dict.insert("ignore_eip_3860".to_string(), true.into());
371 }
372
373 Ok(Map::from([(Config::selected_profile(), dict)]))
374 }
375}