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