forge/cmd/
build.rs

1use super::{install, watch::WatchArgs};
2use clap::Parser;
3use eyre::Result;
4use foundry_cli::{
5    opts::BuildOpts,
6    utils::{cache_local_signatures, LoadConfig},
7};
8use foundry_common::{compile::ProjectCompiler, shell};
9use foundry_compilers::{
10    compilers::{multi::MultiCompilerLanguage, Language},
11    utils::source_files_iter,
12    Project, ProjectCompileOutput,
13};
14use foundry_config::{
15    figment::{
16        self,
17        error::Kind::InvalidType,
18        value::{Dict, Map, Value},
19        Metadata, Profile, Provider,
20    },
21    Config,
22};
23use serde::Serialize;
24use std::path::PathBuf;
25
26foundry_config::merge_impl_figment_convert!(BuildArgs, build);
27
28/// CLI arguments for `forge build`.
29///
30/// CLI arguments take the highest precedence in the Config/Figment hierarchy.
31/// In order to override them in the foundry `Config` they need to be merged into an existing
32/// `figment::Provider`, like `foundry_config::Config` is.
33///
34/// `BuildArgs` implements `figment::Provider` in which all config related fields are serialized and
35/// then merged into an existing `Config`, effectively overwriting them.
36///
37/// Some arguments are marked as `#[serde(skip)]` and require manual processing in
38/// `figment::Provider` implementation
39#[derive(Clone, Debug, Default, Serialize, Parser)]
40#[command(next_help_heading = "Build options", about = None, long_about = None)] // override doc
41pub struct BuildArgs {
42    /// Build source files from specified paths.
43    #[serde(skip)]
44    pub paths: Option<Vec<PathBuf>>,
45
46    /// Print compiled contract names.
47    #[arg(long)]
48    #[serde(skip)]
49    pub names: bool,
50
51    /// Print compiled contract sizes.
52    /// Constructor argument length is not included in the calculation of initcode size.
53    #[arg(long)]
54    #[serde(skip)]
55    pub sizes: bool,
56
57    /// Ignore initcode contract bytecode size limit introduced by EIP-3860.
58    #[arg(long, alias = "ignore-initcode-size")]
59    #[serde(skip)]
60    pub ignore_eip_3860: bool,
61
62    #[command(flatten)]
63    #[serde(flatten)]
64    pub build: BuildOpts,
65
66    #[command(flatten)]
67    #[serde(skip)]
68    pub watch: WatchArgs,
69}
70
71impl BuildArgs {
72    pub fn run(self) -> Result<ProjectCompileOutput> {
73        let mut config = self.load_config()?;
74
75        if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings {
76            // need to re-configure here to also catch additional remappings
77            config = self.load_config()?;
78        }
79
80        let project = config.project()?;
81
82        // Collect sources to compile if build subdirectories specified.
83        let mut files = vec![];
84        if let Some(paths) = &self.paths {
85            for path in paths {
86                let joined = project.root().join(path);
87                let path = if joined.exists() { &joined } else { path };
88                files.extend(source_files_iter(path, MultiCompilerLanguage::FILE_EXTENSIONS));
89            }
90            if files.is_empty() {
91                eyre::bail!("No source files found in specified build paths.")
92            }
93        }
94
95        let format_json = shell::is_json();
96        let compiler = ProjectCompiler::new()
97            .files(files)
98            .dynamic_test_linking(config.dynamic_test_linking)
99            .print_names(self.names)
100            .print_sizes(self.sizes)
101            .ignore_eip_3860(self.ignore_eip_3860)
102            .bail(!format_json);
103
104        let output = compiler.compile(&project)?;
105
106        // Cache project selectors.
107        cache_local_signatures(&output)?;
108
109        if format_json && !self.names && !self.sizes {
110            sh_println!("{}", serde_json::to_string_pretty(&output.output())?)?;
111        }
112
113        Ok(output)
114    }
115
116    /// Returns the `Project` for the current workspace
117    ///
118    /// This loads the `foundry_config::Config` for the current workspace (see
119    /// [`foundry_config::utils::find_project_root`] and merges the cli `BuildArgs` into it before
120    /// returning [`foundry_config::Config::project()`]
121    pub fn project(&self) -> Result<Project> {
122        self.build.project()
123    }
124
125    /// Returns whether `BuildArgs` was configured with `--watch`
126    pub fn is_watch(&self) -> bool {
127        self.watch.watch.is_some()
128    }
129
130    /// Returns the [`watchexec::Config`] necessary to bootstrap a new watch loop.
131    pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
132        // Use the path arguments or if none where provided the `src`, `test` and `script`
133        // directories as well as the `foundry.toml` configuration file.
134        self.watch.watchexec_config(|| {
135            let config = self.load_config()?;
136            let foundry_toml: PathBuf = config.root.join(Config::FILE_NAME);
137            Ok([config.src, config.test, config.script, foundry_toml])
138        })
139    }
140}
141
142// Make this args a `figment::Provider` so that it can be merged into the `Config`
143impl Provider for BuildArgs {
144    fn metadata(&self) -> Metadata {
145        Metadata::named("Build Args Provider")
146    }
147
148    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
149        let value = Value::serialize(self)?;
150        let error = InvalidType(value.to_actual(), "map".into());
151        let mut dict = value.into_dict().ok_or(error)?;
152
153        if self.names {
154            dict.insert("names".to_string(), true.into());
155        }
156
157        if self.sizes {
158            dict.insert("sizes".to_string(), true.into());
159        }
160
161        if self.ignore_eip_3860 {
162            dict.insert("ignore_eip_3860".to_string(), true.into());
163        }
164
165        Ok(Map::from([(Config::selected_profile(), dict)]))
166    }
167}