Skip to main content

foundry_cli/opts/build/
core.rs

1use super::ProjectPathOpts;
2use crate::{opts::CompilerOpts, utils::LoadConfig};
3use clap::{Parser, ValueHint};
4use eyre::Result;
5use foundry_compilers::{
6    Project,
7    artifacts::{RevertStrings, remappings::Remapping},
8    compilers::multi::MultiCompiler,
9    utils::canonicalized,
10};
11use foundry_config::{
12    Config, DenyLevel, Remappings,
13    figment::{
14        self, Figment, Metadata, Profile, Provider,
15        error::Kind::InvalidType,
16        value::{Dict, Map, Value},
17    },
18    filter::SkipBuildFilter,
19    providers::relative_remapping_preserving_context_boundary,
20};
21use serde::Serialize;
22use std::path::PathBuf;
23
24#[derive(Clone, Debug, Default, Serialize, Parser)]
25#[command(next_help_heading = "Build options")]
26pub struct BuildOpts {
27    /// Clear the cache and artifacts folder and recompile.
28    #[arg(long, help_heading = "Cache options")]
29    #[serde(skip)]
30    pub force: bool,
31
32    /// Disable the cache.
33    #[arg(long)]
34    #[serde(skip)]
35    pub no_cache: bool,
36
37    /// Disable dynamic test linking.
38    #[arg(long, conflicts_with = "no_cache")]
39    #[serde(skip)]
40    pub no_dynamic_test_linking: bool,
41
42    /// Reject contracts that depend on externally linked Solidity libraries.
43    ///
44    /// Internal libraries and dynamic test linking remain available. Overrides
45    /// `allow_linked_libraries` in configuration and environment variables.
46    #[arg(long, help_heading = "Linker options")]
47    #[serde(skip)]
48    pub disallow_linked_libraries: bool,
49
50    /// Set pre-linked libraries.
51    #[arg(long, help_heading = "Linker options", env = "DAPP_LIBRARIES")]
52    #[serde(skip_serializing_if = "Vec::is_empty")]
53    pub libraries: Vec<String>,
54
55    /// Ignore solc warnings by error code.
56    #[arg(long, help_heading = "Compiler options", value_name = "ERROR_CODES")]
57    #[serde(skip_serializing_if = "Vec::is_empty")]
58    pub ignored_error_codes: Vec<u64>,
59
60    /// A compiler error will be triggered at the specified diagnostic level.
61    ///
62    /// Replaces the deprecated `--deny-warnings` flag.
63    ///
64    /// Possible values:
65    ///  - `never`: Do not treat any diagnostics as errors.
66    ///  - `warnings`: Treat warnings as errors.
67    ///  - `notes`: Treat both, warnings and notes, as errors.
68    #[arg(
69        long,
70        short = 'D',
71        help_heading = "Compiler options",
72        value_name = "LEVEL",
73        conflicts_with = "deny_warnings"
74    )]
75    #[serde(skip)]
76    pub deny: Option<DenyLevel>,
77
78    /// Deprecated: use `--deny=warnings` instead.
79    #[arg(long = "deny-warnings", hide = true)]
80    pub deny_warnings: bool,
81
82    /// Do not auto-detect the `solc` version.
83    #[arg(long, help_heading = "Compiler options")]
84    #[serde(skip)]
85    pub no_auto_detect: bool,
86
87    /// Specify the solc version, path, or executable name on `PATH` to build with.
88    ///
89    /// Valid values are in the format `x.y.z`, `solc:x.y.z`, `path/to/solc`, or `solc`.
90    #[arg(
91        long = "use",
92        alias = "compiler-version",
93        help_heading = "Compiler options",
94        value_name = "SOLC_VERSION"
95    )]
96    #[serde(skip)]
97    pub use_solc: Option<String>,
98
99    /// Do not access the network.
100    ///
101    /// Missing solc versions will not be installed.
102    #[arg(help_heading = "Compiler options", long)]
103    #[serde(skip)]
104    pub offline: bool,
105
106    /// Changes compilation to only use literal content and not URLs.
107    #[arg(long, help_heading = "Compiler options")]
108    #[serde(skip)]
109    pub use_literal_content: bool,
110
111    /// Do not append any metadata to the bytecode.
112    ///
113    /// This is equivalent to setting `bytecode_hash` to `none` and `cbor_metadata` to `false`.
114    #[arg(long, help_heading = "Compiler options")]
115    #[serde(skip)]
116    pub no_metadata: bool,
117
118    /// The path to the contract artifacts folder.
119    #[arg(
120        long = "out",
121        short,
122        help_heading = "Project options",
123        value_hint = ValueHint::DirPath,
124        value_name = "PATH",
125    )]
126    #[serde(rename = "out", skip_serializing_if = "Option::is_none")]
127    pub out_path: Option<PathBuf>,
128
129    /// Revert string configuration.
130    ///
131    /// Possible values are "default", "strip" (remove),
132    /// "debug" (Solidity-generated revert strings) and "verboseDebug"
133    #[arg(long, help_heading = "Project options", value_name = "REVERT")]
134    #[serde(skip)]
135    pub revert_strings: Option<RevertStrings>,
136
137    /// Generate build info files.
138    #[arg(long, help_heading = "Project options")]
139    #[serde(skip)]
140    pub build_info: bool,
141
142    /// Output path to directory that build info files will be written to.
143    #[arg(
144        long,
145        help_heading = "Project options",
146        value_hint = ValueHint::DirPath,
147        value_name = "PATH",
148        requires = "build_info",
149    )]
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub build_info_path: Option<PathBuf>,
152
153    /// Skip building files whose names contain the given filter.
154    ///
155    /// `test` and `script` are aliases for `.t.sol` and `.s.sol`.
156    #[arg(long, num_args(1..))]
157    #[serde(skip)]
158    pub skip: Option<Vec<SkipBuildFilter>>,
159
160    #[command(flatten)]
161    #[serde(flatten)]
162    pub compiler: CompilerOpts,
163
164    #[command(flatten)]
165    #[serde(flatten)]
166    pub project_paths: ProjectPathOpts,
167}
168
169impl BuildOpts {
170    /// Returns the `Project` for the current workspace
171    ///
172    /// This loads the `foundry_config::Config` for the current workspace (see
173    /// `find_project_root` and merges the cli `BuildArgs` into it before returning
174    /// [`foundry_config::Config::project()`]).
175    pub fn project(&self) -> Result<Project<MultiCompiler>> {
176        let config = self.load_config()?;
177        Ok(config.project()?)
178    }
179
180    /// Returns the remappings to add to the config
181    #[deprecated(note = "Use ProjectPathsArgs::get_remappings() instead")]
182    pub fn get_remappings(&self) -> Vec<Remapping> {
183        self.project_paths.get_remappings()
184    }
185}
186
187// Loads project's figment and merges the build cli arguments into it
188impl<'a> From<&'a BuildOpts> for Figment {
189    fn from(args: &'a BuildOpts) -> Self {
190        let root = args.project_paths.project_root();
191        let mut figment = Config::figment_with_root(&root);
192
193        // remappings should stack
194        let remappings_root = canonicalized(&root);
195        let cli_remappings = args
196            .project_paths
197            .get_remappings()
198            .into_iter()
199            .map(|remapping| {
200                let remapping = relative_remapping_preserving_context_boundary(remapping, &root)
201                    .to_relative_remapping();
202                relative_remapping_preserving_context_boundary(remapping, &remappings_root)
203                    .to_relative_remapping()
204            })
205            .collect();
206        let mut remappings = Remappings::new_with_remappings(cli_remappings).with_figment(&figment);
207        let generated_remappings = Remappings::generated_from_figment(&figment);
208        remappings.extend_with_config_remappings(
209            figment.extract_inner::<Vec<Remapping>>("remappings").unwrap_or_default(),
210            &generated_remappings,
211        );
212        let remappings = remappings
213            .into_inner()
214            .into_iter()
215            .map(|remapping| relative_remapping_preserving_context_boundary(remapping, &root))
216            .collect::<Vec<_>>();
217        figment = figment.merge(("remappings", remappings)).merge(args);
218
219        if let Some(skip) = &args.skip {
220            let mut skip = skip.iter().map(|s| s.file_pattern().to_string()).collect::<Vec<_>>();
221            skip.extend(figment.extract_inner::<Vec<String>>("skip").unwrap_or_default());
222            figment = figment.merge(("skip", skip));
223        };
224
225        figment
226    }
227}
228
229impl Provider for BuildOpts {
230    fn metadata(&self) -> Metadata {
231        Metadata::named("Core Build Args Provider")
232    }
233
234    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
235        let value = Value::serialize(self)?;
236        let error = InvalidType(value.to_actual(), "map".into());
237        let mut dict = value.into_dict().ok_or(error)?;
238
239        if self.no_auto_detect {
240            dict.insert("auto_detect_solc".to_string(), false.into());
241        }
242
243        if let Some(ref solc) = self.use_solc {
244            dict.insert("solc".to_string(), solc.trim_start_matches("solc:").into());
245        }
246
247        if self.offline {
248            dict.insert("offline".to_string(), true.into());
249        }
250
251        if self.deny_warnings {
252            dict.insert("deny".to_string(), figment::value::Value::serialize(DenyLevel::Warnings)?);
253            _ = sh_warn!("`--deny-warnings` is being deprecated in favor of `--deny warnings`.");
254        } else if let Some(deny) = self.deny {
255            dict.insert("deny".to_string(), figment::value::Value::serialize(deny)?);
256        }
257
258        if self.use_literal_content {
259            dict.insert("use_literal_content".to_string(), true.into());
260        }
261
262        if self.no_metadata {
263            dict.insert("bytecode_hash".to_string(), "none".into());
264            dict.insert("cbor_metadata".to_string(), false.into());
265        }
266
267        if self.force {
268            dict.insert("force".to_string(), self.force.into());
269        }
270
271        // we need to ensure no_cache set accordingly
272        if self.no_cache {
273            dict.insert("cache".to_string(), false.into());
274        }
275
276        if self.no_dynamic_test_linking {
277            dict.insert("dynamic_test_linking".to_string(), false.into());
278        }
279
280        if self.build_info {
281            dict.insert("build_info".to_string(), self.build_info.into());
282        }
283
284        if self.compiler.ast {
285            dict.insert("ast".to_string(), true.into());
286        }
287
288        if let Some(optimize) = self.compiler.optimize {
289            dict.insert("optimizer".to_string(), optimize.into());
290        }
291
292        if self.compiler.via_ir {
293            dict.insert("via_ir".to_string(), true.into());
294        }
295
296        if self.compiler.via_ssa_cfg {
297            dict.insert("via_ssa_cfg".to_string(), true.into());
298        }
299
300        if self.compiler.experimental {
301            dict.insert("experimental".to_string(), true.into());
302        }
303
304        if !self.compiler.extra_output.is_empty() {
305            let selection: Vec<_> =
306                self.compiler.extra_output.iter().map(|s| s.to_string()).collect();
307            dict.insert("extra_output".to_string(), selection.into());
308        }
309
310        if !self.compiler.extra_output_files.is_empty() {
311            let selection: Vec<_> =
312                self.compiler.extra_output_files.iter().map(|s| s.to_string()).collect();
313            dict.insert("extra_output_files".to_string(), selection.into());
314        }
315
316        if let Some(ref revert) = self.revert_strings {
317            dict.insert("revert_strings".to_string(), revert.to_string().into());
318        }
319
320        let mut data = Map::from([(Config::selected_profile(), dict)]);
321        if self.disallow_linked_libraries {
322            // Environment variables use the global profile, which wins over named profiles.
323            data.entry(Profile::Global)
324                .or_default()
325                .insert("allow_linked_libraries".to_string(), false.into());
326        }
327        Ok(data)
328    }
329}