1use super::{install, watch::WatchArgs};
2use clap::Parser;
3use eyre::{Context, 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 std::path::PathBuf;
27
28foundry_config::merge_impl_figment_convert!(BuildArgs, build);
29
30#[derive(Clone, Debug, Default, Serialize, Parser)]
42#[command(next_help_heading = "Build options", about = None, long_about = None)] pub struct BuildArgs {
44 #[serde(skip)]
46 pub paths: Option<Vec<PathBuf>>,
47
48 #[arg(long)]
50 #[serde(skip)]
51 pub names: bool,
52
53 #[arg(long)]
56 #[serde(skip)]
57 pub sizes: bool,
58
59 #[arg(long, alias = "ignore-initcode-size")]
61 #[serde(skip)]
62 pub ignore_eip_3860: bool,
63
64 #[command(flatten)]
65 #[serde(flatten)]
66 pub build: BuildOpts,
67
68 #[command(flatten)]
69 #[serde(skip)]
70 pub watch: WatchArgs,
71}
72
73impl BuildArgs {
74 pub async fn run(self) -> Result<ProjectCompileOutput> {
75 let mut config = self.load_config()?;
76
77 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
78 {
79 config = self.load_config()?;
81 }
82
83 self.check_soldeer_lock_consistency(&config).await;
84 self.check_foundry_lock_consistency(&config);
85
86 let project = config.project()?;
87
88 let mut files = vec![];
90 if let Some(paths) = &self.paths {
91 for path in paths {
92 let joined = project.root().join(path);
93 let path = if joined.exists() { &joined } else { path };
94 files.extend(source_files_iter(path, MultiCompilerLanguage::FILE_EXTENSIONS));
95 }
96 if files.is_empty() {
97 eyre::bail!("No source files found in specified build paths.")
98 }
99 }
100
101 let format_json = shell::is_json();
102 let compiler = ProjectCompiler::new()
103 .files(files)
104 .dynamic_test_linking(config.dynamic_test_linking)
105 .print_names(self.names)
106 .print_sizes(self.sizes)
107 .ignore_eip_3860(self.ignore_eip_3860)
108 .bail(!format_json);
109
110 let mut output = compiler.compile(&project)?;
111
112 cache_local_signatures(&output)?;
114
115 if format_json && !self.names && !self.sizes {
116 sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
117 }
118
119 if config.lint.lint_on_build && !output.output().errors.iter().any(|e| e.is_error()) {
121 self.lint(&project, &config, self.paths.as_deref(), &mut output)
122 .wrap_err("Lint failed")?;
123 }
124
125 Ok(output)
126 }
127
128 fn lint(
129 &self,
130 project: &Project,
131 config: &Config,
132 files: Option<&[PathBuf]>,
133 output: &mut ProjectCompileOutput,
134 ) -> Result<()> {
135 let format_json = shell::is_json();
136 if project.compiler.solc.is_some() && !shell::is_quiet() {
137 let linter = SolidityLinter::new(config.project_paths())
138 .with_json_emitter(format_json)
139 .with_description(!format_json)
140 .with_severity(if config.lint.severity.is_empty() {
141 None
142 } else {
143 Some(config.lint.severity.clone())
144 })
145 .without_lints(if config.lint.exclude_lints.is_empty() {
146 None
147 } else {
148 Some(
149 config
150 .lint
151 .exclude_lints
152 .iter()
153 .filter_map(|s| forge_lint::sol::SolLint::try_from(s.as_str()).ok())
154 .collect(),
155 )
156 })
157 .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions);
158
159 let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
161 .iter()
162 .flat_map(foundry_common::fs::canonicalize_path)
163 .collect::<Vec<_>>();
164
165 let skip = SkipBuildFilters::new(config.skip.clone(), config.root.clone());
166 let curr_dir = std::env::current_dir()?;
167 let input_files = config
168 .project_paths::<SolcLanguage>()
169 .input_files_iter()
170 .filter(|p| {
171 if let Some(files) = files {
173 return files.iter().any(|file| &curr_dir.join(file) == p);
174 }
175 skip.is_match(p)
176 && !(ignored.contains(p) || ignored.contains(&curr_dir.join(p)))
177 })
178 .collect::<Vec<_>>();
179
180 let solar_sources =
181 get_solar_sources_from_compile_output(config, output, Some(&input_files), None)?;
182 if solar_sources.input.sources.is_empty() {
183 if !input_files.is_empty() {
184 sh_warn!(
185 "unable to lint. Solar only supports Solidity versions prior to 0.8.0"
186 )?;
187 }
188 return Ok(());
189 }
190
191 let mut compiler = solar::sema::Compiler::new(
194 solar::interface::Session::builder().with_stderr_emitter().build(),
195 );
196
197 compiler.enter_mut(|compiler| {
199 let mut pcx = compiler.parse();
200 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
201 pcx.set_resolve_imports(true);
202 pcx.parse();
203 });
204 linter.lint(&input_files, config.deny, &mut compiler)?;
205 }
206
207 Ok(())
208 }
209
210 pub fn project(&self) -> Result<Project> {
216 self.build.project()
217 }
218
219 pub fn is_watch(&self) -> bool {
221 self.watch.watch.is_some()
222 }
223
224 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
226 self.watch.watchexec_config(|| {
229 let config = self.load_config()?;
230 let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
231 Ok([config.src, config.test, config.script, foundry_toml])
232 })
233 }
234
235 async fn check_soldeer_lock_consistency(&self, config: &Config) {
237 let soldeer_lock_path = config.root.join("soldeer.lock");
238 if !soldeer_lock_path.exists() {
239 return;
240 }
241
242 let Ok(lockfile) = soldeer_core::lock::read_lockfile(&soldeer_lock_path) else {
244 return;
245 };
246
247 let deps_dir = config.root.join("dependencies");
248 for entry in &lockfile.entries {
249 let dep_name = entry.name();
250
251 match soldeer_core::install::check_dependency_integrity(entry, &deps_dir).await {
253 Ok(status) => {
254 use soldeer_core::install::DependencyStatus;
255 if matches!(
257 status,
258 DependencyStatus::Missing | DependencyStatus::FailedIntegrity
259 ) {
260 sh_warn!("Dependency '{}' integrity check failed: {:?}", dep_name, status)
261 .ok();
262 }
263 }
264 Err(e) => {
265 sh_warn!("Dependency '{}' integrity check error: {}", dep_name, e).ok();
266 }
267 }
268 }
269 }
270
271 fn check_foundry_lock_consistency(&self, config: &Config) {
273 use crate::lockfile::{DepIdentifier, FOUNDRY_LOCK, Lockfile};
274
275 let foundry_lock_path = config.root.join(FOUNDRY_LOCK);
276 if !foundry_lock_path.exists() {
277 return;
278 }
279
280 let git = Git::new(&config.root);
281
282 let mut lockfile = Lockfile::new(&config.root).with_git(&git);
283 if let Err(e) = lockfile.read() {
284 if !e.to_string().contains("Lockfile not found") {
285 sh_warn!("Failed to parse foundry.lock: {}", e).ok();
286 }
287 return;
288 }
289
290 for (dep_path, dep_identifier) in lockfile.iter() {
291 let full_path = config.root.join(dep_path);
292
293 if !full_path.exists() {
294 sh_warn!("Dependency '{}' not found at expected path", dep_path.display()).ok();
295 continue;
296 }
297
298 let actual_rev = match git.get_rev("HEAD", &full_path) {
299 Ok(rev) => rev,
300 Err(_) => {
301 sh_warn!("Failed to get git revision for dependency '{}'", dep_path.display())
302 .ok();
303 continue;
304 }
305 };
306
307 let expected_rev = match dep_identifier {
309 DepIdentifier::Branch { rev, .. }
310 | DepIdentifier::Tag { rev, .. }
311 | DepIdentifier::Rev { rev, .. } => rev.clone(),
312 };
313
314 if actual_rev != expected_rev {
315 sh_warn!(
316 "Dependency '{}' revision mismatch: expected '{}', found '{}'",
317 dep_path.display(),
318 expected_rev,
319 actual_rev
320 )
321 .ok();
322 }
323 }
324 }
325}
326
327impl Provider for BuildArgs {
329 fn metadata(&self) -> Metadata {
330 Metadata::named("Build Args Provider")
331 }
332
333 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
334 let value = Value::serialize(self)?;
335 let error = InvalidType(value.to_actual(), "map".into());
336 let mut dict = value.into_dict().ok_or(error)?;
337
338 if self.names {
339 dict.insert("names".to_string(), true.into());
340 }
341
342 if self.sizes {
343 dict.insert("sizes".to_string(), true.into());
344 }
345
346 if self.ignore_eip_3860 {
347 dict.insert("ignore_eip_3860".to_string(), true.into());
348 }
349
350 Ok(Map::from([(Config::selected_profile(), dict)]))
351 }
352}