forge/cmd/
build.rs

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