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