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