1use super::{install, watch::WatchArgs};
2use clap::Parser;
3use eyre::Result;
4use forge_lint::{
5 linter::Linter,
6 sol::{DeniedLintDiagnostics, SolidityLinter},
7};
8use foundry_cli::{
9 opts::{BuildOpts, configure_pcx_from_solc, get_solar_sources_from_compile_output},
10 utils::{Git, LoadConfig, cache_local_signatures},
11};
12use foundry_common::{
13 compile::{ContractSizeLimits, ProjectCompiler},
14 shell,
15};
16use foundry_compilers::{
17 CompilationError, FileFilter, Project, ProjectCompileOutput,
18 compilers::{Language, multi::MultiCompilerLanguage},
19 solc::SolcLanguage,
20 utils::source_files_iter,
21};
22use foundry_config::{
23 Config, SkipBuildFilters,
24 figment::{
25 self, Metadata, Profile, Provider,
26 error::Kind::InvalidType,
27 value::{Dict, Map, Value},
28 },
29 filter::expand_globs,
30};
31use serde::Serialize;
32use solar::{
33 interface::{Session, config::CompileOpts},
34 sema::Compiler,
35};
36use std::path::PathBuf;
37
38foundry_config::merge_impl_figment_convert!(BuildArgs, build);
39
40#[derive(Clone, Debug, Default, Serialize, Parser)]
52#[command(next_help_heading = "Build options", about = None, long_about = None)] pub struct BuildArgs {
54 #[serde(skip)]
56 pub paths: Option<Vec<PathBuf>>,
57
58 #[arg(long)]
60 #[serde(skip)]
61 pub names: bool,
62
63 #[arg(long)]
66 #[serde(skip)]
67 pub sizes: bool,
68
69 #[arg(long, alias = "ignore-initcode-size")]
71 #[serde(skip)]
72 pub ignore_eip_3860: bool,
73
74 #[arg(long, visible_alias = "skip-lint")]
79 #[serde(skip)]
80 pub no_lint: bool,
81
82 #[command(flatten)]
83 #[serde(flatten)]
84 pub build: BuildOpts,
85
86 #[command(flatten)]
87 #[serde(skip)]
88 pub watch: WatchArgs,
89}
90
91impl BuildArgs {
92 pub async fn run(self) -> Result<ProjectCompileOutput> {
93 let mut config = self.load_config()?;
94
95 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
96 {
97 config = self.load_config()?;
99 }
100
101 self.check_soldeer_lock_consistency(&config).await;
102 self.check_foundry_lock_consistency(&config);
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_names(self.names)
125 .print_sizes(self.sizes)
126 .ignore_eip_3860(self.ignore_eip_3860)
127 .size_limits(
128 config
129 .code_size_limit
130 .map(ContractSizeLimits::with_runtime_limit)
131 .unwrap_or_default(),
132 )
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| forge_lint::sol::SolLint::try_from(s.as_str()).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)
209 && !(ignored.contains(p) || ignored.contains(&curr_dir.join(p)))
210 })
211 .collect::<Vec<_>>();
212
213 let solar_sources =
214 get_solar_sources_from_compile_output(config, output, Some(&input_files), None)?;
215 if solar_sources.input.sources.is_empty() {
216 if !input_files.is_empty() {
217 sh_warn!("unable to lint. Solar only supports Solidity versions >=0.8.0")?;
218 }
219 return Ok(());
220 }
221
222 let mut opts = CompileOpts::default();
225 opts.unstable.typeck = true;
226 let mut compiler =
227 Compiler::new(Session::builder().opts(opts).with_stderr_emitter().build());
228
229 compiler.enter_mut(|compiler| {
231 let mut pcx = compiler.parse();
232 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
233 pcx.set_resolve_imports(true);
234 pcx.parse();
235 });
236
237 linter.lint(&input_files, config.deny, &mut compiler)?;
238 }
239
240 Ok(())
241 }
242
243 pub fn project(&self) -> Result<Project> {
249 self.build.project()
250 }
251
252 pub const fn is_watch(&self) -> bool {
254 self.watch.watch.is_some()
255 }
256
257 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
259 self.watch.watchexec_config(|| {
262 let config = self.load_config()?;
263 let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
264 Ok([config.src, config.test, config.script, foundry_toml])
265 })
266 }
267
268 async fn check_soldeer_lock_consistency(&self, config: &Config) {
270 let soldeer_lock_path = config.root.join("soldeer.lock");
271 if !soldeer_lock_path.exists() {
272 return;
273 }
274
275 let Ok(lockfile) = soldeer_core::lock::read_lockfile(&soldeer_lock_path) else {
277 return;
278 };
279
280 let deps_dir = config.root.join("dependencies");
281 for entry in &lockfile.entries {
282 let dep_name = entry.name();
283
284 match soldeer_core::install::check_dependency_integrity(entry, &deps_dir).await {
286 Ok(status) => {
287 use soldeer_core::install::DependencyStatus;
288 if matches!(
290 status,
291 DependencyStatus::Missing | DependencyStatus::FailedIntegrity
292 ) {
293 sh_warn!("Dependency '{}' integrity check failed: {:?}", dep_name, status)
294 .ok();
295 }
296 }
297 Err(e) => {
298 sh_warn!("Dependency '{}' integrity check error: {}", dep_name, e).ok();
299 }
300 }
301 }
302 }
303
304 fn check_foundry_lock_consistency(&self, config: &Config) {
306 use crate::lockfile::{DepIdentifier, FOUNDRY_LOCK, Lockfile};
307
308 let foundry_lock_path = config.root.join(FOUNDRY_LOCK);
309 if !foundry_lock_path.exists() {
310 return;
311 }
312
313 let git = Git::new(&config.root);
314
315 let mut lockfile = Lockfile::new(&config.root).with_git(&git);
316 if let Err(e) = lockfile.read() {
317 if !e.to_string().contains("Lockfile not found") {
318 sh_warn!("Failed to parse foundry.lock: {}", e).ok();
319 }
320 return;
321 }
322
323 for (dep_path, dep_identifier) in lockfile.iter() {
324 let full_path = config.root.join(dep_path);
325
326 if !full_path.exists() {
327 sh_warn!("Dependency '{}' not found at expected path", dep_path.display()).ok();
328 continue;
329 }
330
331 let actual_rev = match git.get_rev("HEAD", &full_path) {
332 Ok(rev) => rev,
333 Err(_) => {
334 sh_warn!("Failed to get git revision for dependency '{}'", dep_path.display())
335 .ok();
336 continue;
337 }
338 };
339
340 let expected_rev = match dep_identifier {
342 DepIdentifier::Branch { rev, .. }
343 | DepIdentifier::Tag { rev, .. }
344 | DepIdentifier::Rev { rev, .. } => rev.clone(),
345 };
346
347 if actual_rev != expected_rev {
348 sh_warn!(
349 "Dependency '{}' revision mismatch: expected '{}', found '{}'",
350 dep_path.display(),
351 expected_rev,
352 actual_rev
353 )
354 .ok();
355 }
356 }
357 }
358}
359
360const LINT_FAILURE_NOTICE: &str = "\
363note: internal lint engine failure (compilation itself succeeded).
364note: please file a bug report at
365 https://github.com/foundry-rs/foundry/issues/new?template=BUG-FORM.yml
366 and attach the full output above.
367help: rerun with `--no-lint` to skip linting for this build, or consider temporarily
368 disabling forge lint on build:
369 https://getfoundry.sh/forge/linting#disable-linting-on-build
370";
371
372fn emit_lint_failure_notice() {
373 if shell::is_json() {
374 return;
375 }
376 let _ = sh_eprintln!("\n{LINT_FAILURE_NOTICE}");
377}
378
379impl Provider for BuildArgs {
381 fn metadata(&self) -> Metadata {
382 Metadata::named("Build Args Provider")
383 }
384
385 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
386 let value = Value::serialize(self)?;
387 let error = InvalidType(value.to_actual(), "map".into());
388 let mut dict = value.into_dict().ok_or(error)?;
389
390 if self.names {
391 dict.insert("names".to_string(), true.into());
392 }
393
394 if self.sizes {
395 dict.insert("sizes".to_string(), true.into());
396 }
397
398 if self.ignore_eip_3860 {
399 dict.insert("ignore_eip_3860".to_string(), true.into());
400 }
401
402 Ok(Map::from([(Config::selected_profile(), dict)]))
403 }
404}