1use super::{install, 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,
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, true)?;
98 }
99
100 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
101 {
102 config = self.load_config()?;
104 }
105
106 self.check_soldeer_lock_consistency(&config).await;
107 if !locked {
108 self.check_foundry_lock_consistency(&config, false)?;
109 }
110
111 let project = config.project()?;
112
113 let mut files = vec![];
115 if let Some(paths) = &self.paths {
116 for path in paths {
117 let joined = project.root().join(path);
118 let path = if joined.exists() { &joined } else { path };
119 files.extend(source_files_iter(path, MultiCompilerLanguage::FILE_EXTENSIONS));
120 }
121 if files.is_empty() {
122 eyre::bail!("No source files found in specified build paths.");
123 }
124 }
125
126 let format_json = shell::is_json();
127
128 let mut output = ProjectCompiler::new()
129 .files(files)
130 .dynamic_test_linking(config.dynamic_test_linking)
131 .print_names(self.names)
132 .print_sizes(self.sizes)
133 .ignore_eip_3860(self.ignore_eip_3860)
134 .size_limits(
135 config
136 .code_size_limit
137 .map(ContractSizeLimits::with_runtime_limit)
138 .unwrap_or_else(|| ContractSizeLimits::for_spec_id(config.evm_spec_id())),
139 )
140 .bail(!format_json)
141 .compile(&project)?;
142
143 cache_local_signatures(&output)?;
145
146 if format_json && (!self.names && !self.sizes || output.has_compiler_errors()) {
147 sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
148 }
149 if format_json && output.has_compiler_errors() {
150 std::process::exit(1);
151 }
152
153 if !self.no_lint
155 && config.lint.lint_on_build
156 && !output.output().errors.iter().any(|e| e.is_error())
157 && let Err(err) = self.lint(&project, &config, self.paths.as_deref(), &mut output)
158 {
159 if err.downcast_ref::<DeniedLintDiagnostics>().is_none() {
160 emit_lint_failure_notice();
161 }
162 return Err(err.wrap_err("post-build lint step failed"));
163 }
164
165 Ok(output)
166 }
167
168 fn lint(
169 &self,
170 project: &Project,
171 config: &Config,
172 files: Option<&[PathBuf]>,
173 output: &mut ProjectCompileOutput,
174 ) -> Result<()> {
175 let format_json = shell::is_json();
176 if project.compiler.solc.is_some() && !shell::is_quiet() {
177 let linter = SolidityLinter::new(config.project_paths())
178 .with_json_emitter(format_json)
179 .with_description(!format_json)
180 .with_severity(if config.lint.severity.is_empty() {
181 None
182 } else {
183 Some(config.lint.severity.clone())
184 })
185 .without_lints(if config.lint.exclude_lints.is_empty() {
186 None
187 } else {
188 Some(
189 config
190 .lint
191 .exclude_lints
192 .iter()
193 .filter_map(|s| forge_lint::sol::SolLint::try_from(s.as_str()).ok())
194 .collect(),
195 )
196 })
197 .with_lint_specific(&config.lint.lint_specific);
198
199 let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
201 .iter()
202 .flat_map(foundry_common::fs::canonicalize_path)
203 .collect::<Vec<_>>();
204
205 let skip = SkipBuildFilters::new(config.skip.clone(), config.root.clone());
206 let curr_dir = std::env::current_dir()?;
207 let input_files = config
208 .project_paths::<SolcLanguage>()
209 .input_files_iter()
210 .filter(|p| {
211 if let Some(files) = files {
213 return files.iter().any(|file| &curr_dir.join(file) == p);
214 }
215 skip.is_match(p)
216 && !(ignored.contains(p) || ignored.contains(&curr_dir.join(p)))
217 })
218 .collect::<Vec<_>>();
219
220 let solar_sources =
221 get_solar_sources_from_compile_output(config, output, Some(&input_files), None)?;
222 if solar_sources.input.sources.is_empty() {
223 if !input_files.is_empty() {
224 sh_warn!("unable to lint. Solar only supports Solidity versions >=0.8.0")?;
225 }
226 return Ok(());
227 }
228
229 let opts = CompileOpts::default();
232 let mut compiler =
233 Compiler::new(Session::builder().opts(opts).with_stderr_emitter().build());
234
235 compiler.enter_mut(|compiler| {
237 let mut pcx = compiler.parse();
238 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
239 pcx.set_resolve_imports(true);
240 pcx.parse();
241 });
242
243 linter.lint(&input_files, config.deny, &mut compiler)?;
244 }
245
246 Ok(())
247 }
248
249 pub fn project(&self) -> Result<Project> {
255 self.build.project()
256 }
257
258 pub const fn is_watch(&self) -> bool {
260 self.watch.watch.is_some()
261 }
262
263 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
265 self.watch.watchexec_config(|| {
268 let config = self.load_config()?;
269 let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
270 Ok([config.src, config.test, config.script, foundry_toml])
271 })
272 }
273
274 async fn check_soldeer_lock_consistency(&self, config: &Config) {
276 let soldeer_lock_path = config.root.join("soldeer.lock");
277 if !soldeer_lock_path.exists() {
278 return;
279 }
280
281 let Ok(lockfile) = soldeer_core::lock::read_lockfile(&soldeer_lock_path) else {
283 return;
284 };
285
286 let deps_dir = config.root.join("dependencies");
287 for entry in &lockfile.entries {
288 let dep_name = entry.name();
289
290 match soldeer_core::install::check_dependency_integrity(entry, &deps_dir).await {
292 Ok(status) => {
293 use soldeer_core::install::DependencyStatus;
294 if matches!(
296 status,
297 DependencyStatus::Missing | DependencyStatus::FailedIntegrity
298 ) {
299 sh_warn!("Dependency '{}' integrity check failed: {:?}", dep_name, status)
300 .ok();
301 }
302 }
303 Err(e) => {
304 sh_warn!("Dependency '{}' integrity check error: {}", dep_name, e).ok();
305 }
306 }
307 }
308 }
309
310 fn check_foundry_lock_consistency(&self, config: &Config, locked: bool) -> Result<()> {
312 let git = Git::new(&config.root);
313 let mut lockfile = Lockfile::new(&config.root).with_git(&git);
314 let mismatches = match lockfile.check() {
315 Ok(mismatches) => mismatches,
316 Err(err) if locked => return Err(err),
317 Err(err) => {
318 sh_warn!("Failed to check foundry.lock: {err}")?;
319 return Ok(());
320 }
321 };
322 if mismatches.is_empty() {
323 return Ok(());
324 }
325
326 if locked {
327 let mut message = String::from("foundry.lock does not match installed dependencies:");
328 for mismatch in mismatches {
329 write!(message, "\n {mismatch}")?;
330 }
331 return Err(eyre::eyre!(message));
332 }
333
334 for mismatch in mismatches {
335 sh_warn!("{mismatch}")?;
336 }
337 Ok(())
338 }
339}
340
341const LINT_FAILURE_NOTICE: &str = "\
344note: internal lint engine failure (compilation itself succeeded).
345note: please file a bug report at
346 https://github.com/foundry-rs/foundry/issues/new?template=BUG-FORM.yml
347 and attach the full output above.
348help: rerun with `--no-lint` to skip linting for this build, or consider temporarily
349 disabling forge lint on build:
350 https://getfoundry.sh/forge/linting#disable-linting-on-build
351";
352
353fn emit_lint_failure_notice() {
354 if shell::is_json() {
355 return;
356 }
357 let _ = sh_eprintln!("\n{LINT_FAILURE_NOTICE}");
358}
359
360impl Provider for BuildArgs {
362 fn metadata(&self) -> Metadata {
363 Metadata::named("Build Args Provider")
364 }
365
366 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
367 let value = Value::serialize(self)?;
368 let error = InvalidType(value.to_actual(), "map".into());
369 let mut dict = value.into_dict().ok_or(error)?;
370
371 if self.names {
372 dict.insert("names".to_string(), true.into());
373 }
374
375 if self.sizes {
376 dict.insert("sizes".to_string(), true.into());
377 }
378
379 if self.ignore_eip_3860 {
380 dict.insert("ignore_eip_3860".to_string(), true.into());
381 }
382
383 Ok(Map::from([(Config::selected_profile(), dict)]))
384 }
385}