1use super::{install, watch::WatchArgs};
2use crate::diagnostic::build::SOLC_ERROR;
3use clap::Parser;
4use eyre::Result;
5use forge_lint::{linter::Linter, sol::SolidityLinter};
6use foundry_cli::{
7 ExitCode,
8 json::{JsonEnvelope, JsonMessage, print_json},
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, DenyLevel, 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::Opts},
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 fn ensure_machine_compatible(&self) {
96 if !foundry_cli::is_machine() {
97 return;
98 }
99 let unsupported =
100 [("--watch", self.is_watch()), ("--names", self.names), ("--sizes", self.sizes)]
101 .into_iter()
102 .filter_map(|(name, on)| on.then_some(name))
103 .collect::<Vec<_>>();
104 if !unsupported.is_empty() {
105 foundry_cli::machine::bail_machine_usage_with_details(
106 format!(
107 "`forge build` under `--machine` does not yet support {}; \
108 run without `--machine` or omit those flags.",
109 unsupported.join(", ")
110 ),
111 serde_json::json!({ "unsupported_flags": unsupported }),
112 );
113 }
114 }
115
116 pub async fn run(self) -> Result<ProjectCompileOutput> {
117 self.ensure_machine_compatible();
118
119 let machine_mode = foundry_cli::is_machine();
120 let mut config = self.load_config()?;
121
122 if !machine_mode
125 && install::install_missing_dependencies(&mut config).await
126 && config.auto_detect_remappings
127 {
128 config = self.load_config()?;
130 }
131
132 if !machine_mode {
135 self.check_soldeer_lock_consistency(&config).await;
136 self.check_foundry_lock_consistency(&config);
137 }
138
139 let project = config.project()?;
140
141 let mut files = vec![];
143 if let Some(paths) = &self.paths {
144 for path in paths {
145 let joined = project.root().join(path);
146 let path = if joined.exists() { &joined } else { path };
147 files.extend(source_files_iter(path, MultiCompilerLanguage::FILE_EXTENSIONS));
148 }
149 if files.is_empty() {
150 eyre::bail!("No source files found in specified build paths.")
151 }
152 }
153
154 let format_json = shell::is_json();
155
156 if machine_mode && !project.paths.has_input_files() && self.paths.is_none() {
159 let payload = BuildData { artifacts: 0, errors: 0, warnings: 0, unchanged: false };
160 print_json(&JsonEnvelope::success(payload))?;
161 std::process::exit(ExitCode::Success.to_i32());
162 }
163
164 let mut compiler = ProjectCompiler::new()
168 .files(files)
169 .dynamic_test_linking(config.dynamic_test_linking)
170 .print_names(self.names)
171 .print_sizes(self.sizes)
172 .ignore_eip_3860(self.ignore_eip_3860)
173 .size_limits(
174 config
175 .code_size_limit
176 .map(ContractSizeLimits::with_runtime_limit)
177 .unwrap_or_default(),
178 )
179 .bail(!format_json && !machine_mode);
180 if machine_mode {
181 compiler = compiler.quiet(true);
182 }
183
184 let mut output = compiler.compile(&project)?;
185
186 if machine_mode && output.has_compiler_errors() {
189 let errors: Vec<JsonMessage> = output
190 .output()
191 .errors
192 .iter()
193 .filter(|e| e.is_error())
194 .map(|e| JsonMessage::error(SOLC_ERROR, e.to_string()))
195 .collect();
196 let _ = print_json(&JsonEnvelope::<()>::failure(errors));
199 std::process::exit(ExitCode::Build.to_i32());
200 }
201
202 cache_local_signatures(&output)?;
204
205 if format_json && !self.names && !self.sizes && !machine_mode {
206 sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
207 }
208
209 if machine_mode {
210 if !self.no_lint && config.lint.lint_on_build && config.deny != DenyLevel::Never {
214 foundry_cli::machine::bail_machine_usage(
215 "`forge build --machine` does not model lint diagnostics in v1; \
216 `lint_on_build = true` with `deny != never` would diverge human and \
217 machine outcomes. Pass `--no-lint` or set `deny = never`.",
218 );
219 }
220
221 let payload = BuildData::from_output(&output);
222 print_json(&JsonEnvelope::success(payload))?;
223 return Ok(output);
224 }
225
226 if !self.no_lint
228 && config.lint.lint_on_build
229 && !output.output().errors.iter().any(|e| e.is_error())
230 && let Err(err) = self.lint(&project, &config, self.paths.as_deref(), &mut output)
231 {
232 emit_lint_failure_notice();
233 return Err(err.wrap_err("post-build lint step failed"));
234 }
235
236 Ok(output)
237 }
238
239 fn lint(
240 &self,
241 project: &Project,
242 config: &Config,
243 files: Option<&[PathBuf]>,
244 output: &mut ProjectCompileOutput,
245 ) -> Result<()> {
246 let format_json = shell::is_json();
247 if project.compiler.solc.is_some() && !shell::is_quiet() {
248 let linter = SolidityLinter::new(config.project_paths())
249 .with_json_emitter(format_json)
250 .with_description(!format_json)
251 .with_severity(if config.lint.severity.is_empty() {
252 None
253 } else {
254 Some(config.lint.severity.clone())
255 })
256 .without_lints(if config.lint.exclude_lints.is_empty() {
257 None
258 } else {
259 Some(
260 config
261 .lint
262 .exclude_lints
263 .iter()
264 .filter_map(|s| forge_lint::sol::SolLint::try_from(s.as_str()).ok())
265 .collect(),
266 )
267 })
268 .with_lint_specific(&config.lint.lint_specific);
269
270 let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
272 .iter()
273 .flat_map(foundry_common::fs::canonicalize_path)
274 .collect::<Vec<_>>();
275
276 let skip = SkipBuildFilters::new(config.skip.clone(), config.root.clone());
277 let curr_dir = std::env::current_dir()?;
278 let input_files = config
279 .project_paths::<SolcLanguage>()
280 .input_files_iter()
281 .filter(|p| {
282 if let Some(files) = files {
284 return files.iter().any(|file| &curr_dir.join(file) == p);
285 }
286 skip.is_match(p)
287 && !(ignored.contains(p) || ignored.contains(&curr_dir.join(p)))
288 })
289 .collect::<Vec<_>>();
290
291 let solar_sources =
292 get_solar_sources_from_compile_output(config, output, Some(&input_files), None)?;
293 if solar_sources.input.sources.is_empty() {
294 if !input_files.is_empty() {
295 sh_warn!("unable to lint. Solar only supports Solidity versions >=0.8.0")?;
296 }
297 return Ok(());
298 }
299
300 let mut opts = Opts::default();
303 opts.unstable.typeck = true;
304 let mut compiler =
305 Compiler::new(Session::builder().opts(opts).with_stderr_emitter().build());
306
307 compiler.enter_mut(|compiler| {
309 let mut pcx = compiler.parse();
310 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
311 pcx.set_resolve_imports(true);
312 pcx.parse();
313 });
314
315 linter.lint(&input_files, config.deny, &mut compiler)?;
316 }
317
318 Ok(())
319 }
320
321 pub fn project(&self) -> Result<Project> {
327 self.build.project()
328 }
329
330 pub const fn is_watch(&self) -> bool {
332 self.watch.watch.is_some()
333 }
334
335 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
337 self.watch.watchexec_config(|| {
340 let config = self.load_config()?;
341 let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
342 Ok([config.src, config.test, config.script, foundry_toml])
343 })
344 }
345
346 async fn check_soldeer_lock_consistency(&self, config: &Config) {
348 let soldeer_lock_path = config.root.join("soldeer.lock");
349 if !soldeer_lock_path.exists() {
350 return;
351 }
352
353 let Ok(lockfile) = soldeer_core::lock::read_lockfile(&soldeer_lock_path) else {
355 return;
356 };
357
358 let deps_dir = config.root.join("dependencies");
359 for entry in &lockfile.entries {
360 let dep_name = entry.name();
361
362 match soldeer_core::install::check_dependency_integrity(entry, &deps_dir).await {
364 Ok(status) => {
365 use soldeer_core::install::DependencyStatus;
366 if matches!(
368 status,
369 DependencyStatus::Missing | DependencyStatus::FailedIntegrity
370 ) {
371 sh_warn!("Dependency '{}' integrity check failed: {:?}", dep_name, status)
372 .ok();
373 }
374 }
375 Err(e) => {
376 sh_warn!("Dependency '{}' integrity check error: {}", dep_name, e).ok();
377 }
378 }
379 }
380 }
381
382 fn check_foundry_lock_consistency(&self, config: &Config) {
384 use crate::lockfile::{DepIdentifier, FOUNDRY_LOCK, Lockfile};
385
386 let foundry_lock_path = config.root.join(FOUNDRY_LOCK);
387 if !foundry_lock_path.exists() {
388 return;
389 }
390
391 let git = Git::new(&config.root);
392
393 let mut lockfile = Lockfile::new(&config.root).with_git(&git);
394 if let Err(e) = lockfile.read() {
395 if !e.to_string().contains("Lockfile not found") {
396 sh_warn!("Failed to parse foundry.lock: {}", e).ok();
397 }
398 return;
399 }
400
401 for (dep_path, dep_identifier) in lockfile.iter() {
402 let full_path = config.root.join(dep_path);
403
404 if !full_path.exists() {
405 sh_warn!("Dependency '{}' not found at expected path", dep_path.display()).ok();
406 continue;
407 }
408
409 let actual_rev = match git.get_rev("HEAD", &full_path) {
410 Ok(rev) => rev,
411 Err(_) => {
412 sh_warn!("Failed to get git revision for dependency '{}'", dep_path.display())
413 .ok();
414 continue;
415 }
416 };
417
418 let expected_rev = match dep_identifier {
420 DepIdentifier::Branch { rev, .. }
421 | DepIdentifier::Tag { rev, .. }
422 | DepIdentifier::Rev { rev, .. } => rev.clone(),
423 };
424
425 if actual_rev != expected_rev {
426 sh_warn!(
427 "Dependency '{}' revision mismatch: expected '{}', found '{}'",
428 dep_path.display(),
429 expected_rev,
430 actual_rev
431 )
432 .ok();
433 }
434 }
435 }
436}
437
438#[derive(Clone, Debug, Serialize)]
440pub struct BuildData {
441 pub artifacts: usize,
443 pub errors: usize,
445 pub warnings: usize,
447 pub unchanged: bool,
450}
451
452impl BuildData {
453 fn from_output(output: &ProjectCompileOutput) -> Self {
454 let artifacts = output.artifact_ids().count();
455 let mut errors = 0usize;
456 let mut warnings = 0usize;
457 for diag in &output.output().errors {
458 if diag.is_error() {
459 errors += 1;
460 } else {
461 warnings += 1;
462 }
463 }
464 Self { artifacts, errors, warnings, unchanged: output.is_unchanged() }
465 }
466}
467
468const LINT_FAILURE_NOTICE: &str = "\
471note: internal lint engine failure (compilation itself succeeded).
472note: please file a bug report at
473 https://github.com/foundry-rs/foundry/issues/new?template=BUG-FORM.yml
474 and attach the full output above.
475help: rerun with `--no-lint` to skip linting for this build, or consider temporarily
476 disabling forge lint on build:
477 https://getfoundry.sh/forge/linting#disable-linting-on-build
478";
479
480fn emit_lint_failure_notice() {
481 if shell::is_json() {
482 return;
483 }
484 let _ = sh_eprintln!("\n{LINT_FAILURE_NOTICE}");
485}
486
487impl Provider for BuildArgs {
489 fn metadata(&self) -> Metadata {
490 Metadata::named("Build Args Provider")
491 }
492
493 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
494 let value = Value::serialize(self)?;
495 let error = InvalidType(value.to_actual(), "map".into());
496 let mut dict = value.into_dict().ok_or(error)?;
497
498 if self.names {
499 dict.insert("names".to_string(), true.into());
500 }
501
502 if self.sizes {
503 dict.insert("sizes".to_string(), true.into());
504 }
505
506 if self.ignore_eip_3860 {
507 dict.insert("ignore_eip_3860".to_string(), true.into());
508 }
509
510 Ok(Map::from([(Config::selected_profile(), dict)]))
511 }
512}