1use super::{install, watch::WatchArgs};
2use clap::Parser;
3use eyre::Result;
4use forge_lint::{linter::Linter, sol::SolidityLinter};
5use foundry_cli::{
6 opts::{BuildOpts, configure_pcx_from_solc, get_solar_sources_from_compile_output},
7 utils::{Git, LoadConfig, cache_local_signatures},
8};
9use foundry_common::{compile::ProjectCompiler, shell};
10use foundry_compilers::{
11 CompilationError, FileFilter, Project, ProjectCompileOutput,
12 compilers::{Language, multi::MultiCompilerLanguage},
13 solc::SolcLanguage,
14 utils::source_files_iter,
15};
16use foundry_config::{
17 Config, SkipBuildFilters,
18 figment::{
19 self, Metadata, Profile, Provider,
20 error::Kind::InvalidType,
21 value::{Dict, Map, Value},
22 },
23 filter::expand_globs,
24};
25use serde::Serialize;
26use solar::{interface::Session, sema::Compiler};
27use std::path::PathBuf;
28
29foundry_config::merge_impl_figment_convert!(BuildArgs, build);
30
31#[derive(Clone, Debug, Default, Serialize, Parser)]
43#[command(next_help_heading = "Build options", about = None, long_about = None)] pub struct BuildArgs {
45 #[serde(skip)]
47 pub paths: Option<Vec<PathBuf>>,
48
49 #[arg(long)]
51 #[serde(skip)]
52 pub names: bool,
53
54 #[arg(long)]
57 #[serde(skip)]
58 pub sizes: bool,
59
60 #[arg(long, alias = "ignore-initcode-size")]
62 #[serde(skip)]
63 pub ignore_eip_3860: bool,
64
65 #[arg(long, visible_alias = "skip-lint")]
70 #[serde(skip)]
71 pub no_lint: bool,
72
73 #[command(flatten)]
74 #[serde(flatten)]
75 pub build: BuildOpts,
76
77 #[command(flatten)]
78 #[serde(skip)]
79 pub watch: WatchArgs,
80}
81
82impl BuildArgs {
83 pub async fn run(self) -> Result<ProjectCompileOutput> {
84 let mut config = self.load_config()?;
85
86 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
87 {
88 config = self.load_config()?;
90 }
91
92 self.check_soldeer_lock_consistency(&config).await;
93 self.check_foundry_lock_consistency(&config);
94
95 let project = config.project()?;
96
97 let mut files = vec![];
99 if let Some(paths) = &self.paths {
100 for path in paths {
101 let joined = project.root().join(path);
102 let path = if joined.exists() { &joined } else { path };
103 files.extend(source_files_iter(path, MultiCompilerLanguage::FILE_EXTENSIONS));
104 }
105 if files.is_empty() {
106 eyre::bail!("No source files found in specified build paths.")
107 }
108 }
109
110 let format_json = shell::is_json();
111 let compiler = ProjectCompiler::new()
112 .files(files)
113 .dynamic_test_linking(config.dynamic_test_linking)
114 .print_names(self.names)
115 .print_sizes(self.sizes)
116 .ignore_eip_3860(self.ignore_eip_3860)
117 .bail(!format_json);
118
119 let mut output = compiler.compile(&project)?;
120
121 cache_local_signatures(&output)?;
123
124 if format_json && !self.names && !self.sizes {
125 sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
126 }
127
128 if !self.no_lint
130 && config.lint.lint_on_build
131 && !output.output().errors.iter().any(|e| e.is_error())
132 && let Err(err) = self.lint(&project, &config, self.paths.as_deref(), &mut output)
133 {
134 emit_lint_failure_notice();
135 return Err(err.wrap_err("post-build lint step failed"));
136 }
137
138 Ok(output)
139 }
140
141 fn lint(
142 &self,
143 project: &Project,
144 config: &Config,
145 files: Option<&[PathBuf]>,
146 output: &mut ProjectCompileOutput,
147 ) -> Result<()> {
148 let format_json = shell::is_json();
149 if project.compiler.solc.is_some() && !shell::is_quiet() {
150 let linter = SolidityLinter::new(config.project_paths())
151 .with_json_emitter(format_json)
152 .with_description(!format_json)
153 .with_severity(if config.lint.severity.is_empty() {
154 None
155 } else {
156 Some(config.lint.severity.clone())
157 })
158 .without_lints(if config.lint.exclude_lints.is_empty() {
159 None
160 } else {
161 Some(
162 config
163 .lint
164 .exclude_lints
165 .iter()
166 .filter_map(|s| forge_lint::sol::SolLint::try_from(s.as_str()).ok())
167 .collect(),
168 )
169 })
170 .with_lint_specific(&config.lint.lint_specific);
171
172 let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
174 .iter()
175 .flat_map(foundry_common::fs::canonicalize_path)
176 .collect::<Vec<_>>();
177
178 let skip = SkipBuildFilters::new(config.skip.clone(), config.root.clone());
179 let curr_dir = std::env::current_dir()?;
180 let input_files = config
181 .project_paths::<SolcLanguage>()
182 .input_files_iter()
183 .filter(|p| {
184 if let Some(files) = files {
186 return files.iter().any(|file| &curr_dir.join(file) == p);
187 }
188 skip.is_match(p)
189 && !(ignored.contains(p) || ignored.contains(&curr_dir.join(p)))
190 })
191 .collect::<Vec<_>>();
192
193 let solar_sources =
194 get_solar_sources_from_compile_output(config, output, Some(&input_files), None)?;
195 if solar_sources.input.sources.is_empty() {
196 if !input_files.is_empty() {
197 sh_warn!("unable to lint. Solar only supports Solidity versions >=0.8.0")?;
198 }
199 return Ok(());
200 }
201
202 let mut compiler = Compiler::new(Session::builder().with_stderr_emitter().build());
205
206 compiler.enter_mut(|compiler| {
208 let mut pcx = compiler.parse();
209 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
210 pcx.set_resolve_imports(true);
211 pcx.parse();
212 });
213
214 linter.lint(&input_files, config.deny, &mut compiler)?;
215 }
216
217 Ok(())
218 }
219
220 pub fn project(&self) -> Result<Project> {
226 self.build.project()
227 }
228
229 pub const fn is_watch(&self) -> bool {
231 self.watch.watch.is_some()
232 }
233
234 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
236 self.watch.watchexec_config(|| {
239 let config = self.load_config()?;
240 let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
241 Ok([config.src, config.test, config.script, foundry_toml])
242 })
243 }
244
245 async fn check_soldeer_lock_consistency(&self, config: &Config) {
247 let soldeer_lock_path = config.root.join("soldeer.lock");
248 if !soldeer_lock_path.exists() {
249 return;
250 }
251
252 let Ok(lockfile) = soldeer_core::lock::read_lockfile(&soldeer_lock_path) else {
254 return;
255 };
256
257 let deps_dir = config.root.join("dependencies");
258 for entry in &lockfile.entries {
259 let dep_name = entry.name();
260
261 match soldeer_core::install::check_dependency_integrity(entry, &deps_dir).await {
263 Ok(status) => {
264 use soldeer_core::install::DependencyStatus;
265 if matches!(
267 status,
268 DependencyStatus::Missing | DependencyStatus::FailedIntegrity
269 ) {
270 sh_warn!("Dependency '{}' integrity check failed: {:?}", dep_name, status)
271 .ok();
272 }
273 }
274 Err(e) => {
275 sh_warn!("Dependency '{}' integrity check error: {}", dep_name, e).ok();
276 }
277 }
278 }
279 }
280
281 fn check_foundry_lock_consistency(&self, config: &Config) {
283 use crate::lockfile::{DepIdentifier, FOUNDRY_LOCK, Lockfile};
284
285 let foundry_lock_path = config.root.join(FOUNDRY_LOCK);
286 if !foundry_lock_path.exists() {
287 return;
288 }
289
290 let git = Git::new(&config.root);
291
292 let mut lockfile = Lockfile::new(&config.root).with_git(&git);
293 if let Err(e) = lockfile.read() {
294 if !e.to_string().contains("Lockfile not found") {
295 sh_warn!("Failed to parse foundry.lock: {}", e).ok();
296 }
297 return;
298 }
299
300 for (dep_path, dep_identifier) in lockfile.iter() {
301 let full_path = config.root.join(dep_path);
302
303 if !full_path.exists() {
304 sh_warn!("Dependency '{}' not found at expected path", dep_path.display()).ok();
305 continue;
306 }
307
308 let actual_rev = match git.get_rev("HEAD", &full_path) {
309 Ok(rev) => rev,
310 Err(_) => {
311 sh_warn!("Failed to get git revision for dependency '{}'", dep_path.display())
312 .ok();
313 continue;
314 }
315 };
316
317 let expected_rev = match dep_identifier {
319 DepIdentifier::Branch { rev, .. }
320 | DepIdentifier::Tag { rev, .. }
321 | DepIdentifier::Rev { rev, .. } => rev.clone(),
322 };
323
324 if actual_rev != expected_rev {
325 sh_warn!(
326 "Dependency '{}' revision mismatch: expected '{}', found '{}'",
327 dep_path.display(),
328 expected_rev,
329 actual_rev
330 )
331 .ok();
332 }
333 }
334 }
335}
336
337const LINT_FAILURE_NOTICE: &str = "\
340note: internal lint engine failure (compilation itself succeeded).
341note: please file a bug report at
342 https://github.com/foundry-rs/foundry/issues/new?template=BUG-FORM.yml
343 and attach the full output above.
344help: rerun with `--no-lint` to skip linting for this build, or consider temporarily
345 disabling forge lint on build:
346 https://getfoundry.sh/forge/linting#disable-linting-on-build
347";
348
349fn emit_lint_failure_notice() {
350 if shell::is_json() {
351 return;
352 }
353 let _ = sh_eprintln!("\n{LINT_FAILURE_NOTICE}");
354}
355
356impl Provider for BuildArgs {
358 fn metadata(&self) -> Metadata {
359 Metadata::named("Build Args Provider")
360 }
361
362 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
363 let value = Value::serialize(self)?;
364 let error = InvalidType(value.to_actual(), "map".into());
365 let mut dict = value.into_dict().ok_or(error)?;
366
367 if self.names {
368 dict.insert("names".to_string(), true.into());
369 }
370
371 if self.sizes {
372 dict.insert("sizes".to_string(), true.into());
373 }
374
375 if self.ignore_eip_3860 {
376 dict.insert("ignore_eip_3860".to_string(), true.into());
377 }
378
379 Ok(Map::from([(Config::selected_profile(), dict)]))
380 }
381}