Skip to main content

forge/cmd/
compiler.rs

1use clap::{Parser, Subcommand, ValueHint};
2use eyre::Result;
3use foundry_cli::install;
4use foundry_common::shell;
5use foundry_compilers::{
6    Graph, Project,
7    artifacts::EvmVersion,
8    compilers::{
9        multi::{MultiCompiler, MultiCompilerLanguage},
10        solc::{Solc, SolcCompiler},
11    },
12};
13use foundry_config::Config;
14use semver::Version;
15use serde::Serialize;
16use std::{collections::BTreeMap, path::PathBuf};
17
18/// CLI arguments for `forge compiler`.
19#[derive(Debug, Parser)]
20pub struct CompilerArgs {
21    #[command(subcommand)]
22    pub sub: CompilerSubcommands,
23}
24
25impl CompilerArgs {
26    pub fn run(self) -> Result<()> {
27        match self.sub {
28            CompilerSubcommands::Resolve(args) => args.run(),
29        }
30    }
31}
32
33#[derive(Debug, Subcommand)]
34pub enum CompilerSubcommands {
35    /// Retrieves the resolved version(s) of the compiler within the project.
36    #[command(visible_alias = "r")]
37    Resolve(ResolveArgs),
38}
39
40/// Resolved compiler within the project.
41#[derive(Serialize)]
42struct ResolvedCompiler {
43    /// Compiler language.
44    #[serde(skip)]
45    language: MultiCompilerLanguage,
46    /// Compiler version.
47    version: Version,
48    /// Max supported EVM version of compiler.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    evm_version: Option<EvmVersion>,
51    /// Source paths.
52    #[serde(skip_serializing_if = "Vec::is_empty")]
53    paths: Vec<String>,
54}
55
56/// CLI arguments for `forge compiler resolve`.
57#[derive(Debug, Parser)]
58pub struct ResolveArgs {
59    /// The root directory
60    #[arg(long, short, value_hint = ValueHint::DirPath, value_name = "PATH")]
61    root: Option<PathBuf>,
62
63    /// Skip files that match the given regex pattern.
64    #[arg(long, short, value_name = "REGEX")]
65    skip: Option<regex::Regex>,
66
67    /// Print only the executable path for a single resolved compiler.
68    #[arg(long)]
69    path: bool,
70}
71
72impl ResolveArgs {
73    pub fn run(self) -> Result<()> {
74        let Self { root, skip, path } = self;
75
76        let root = root.unwrap_or_else(|| PathBuf::from("."));
77        let mut config = Config::load_with_root(&root)?;
78        install::install_missing_dependencies(&mut config, || Config::load_with_root(&root))?;
79        let project = config.project()?;
80
81        let graph = Graph::resolve(&project.paths)?;
82        let sources = graph.into_sources_by_version(&project)?.sources;
83
84        let mut output: BTreeMap<String, Vec<ResolvedCompiler>> = BTreeMap::new();
85
86        for (language, sources) in sources {
87            let mut versions_with_paths: Vec<ResolvedCompiler> = sources
88                .iter()
89                .map(|(version, sources, _)| {
90                    let paths: Vec<String> = sources
91                        .keys()
92                        .filter_map(|path_file| {
93                            let path_str = path_file
94                                .strip_prefix(&project.paths.root)
95                                .unwrap_or(path_file)
96                                .to_path_buf()
97                                .display()
98                                .to_string();
99
100                            // Skip files that match the given regex pattern.
101                            if let Some(ref regex) = skip
102                                && regex.is_match(&path_str)
103                            {
104                                return None;
105                            }
106
107                            Some(path_str)
108                        })
109                        .collect();
110
111                    let evm_version = (shell::verbosity() > 1).then(|| {
112                        EvmVersion::default().normalize_version_solc(version).unwrap_or_default()
113                    });
114
115                    ResolvedCompiler { language, version: version.clone(), evm_version, paths }
116                })
117                .filter(|version| !version.paths.is_empty())
118                .collect();
119
120            // Sort by SemVer version.
121            versions_with_paths.sort_by(|v1, v2| Version::cmp(&v1.version, &v2.version));
122
123            // Skip language if no paths are found after filtering.
124            if !versions_with_paths.is_empty() {
125                // Clear paths if verbosity is 0, performed only after filtering to avoid being
126                // skipped.
127                if shell::verbosity() == 0 {
128                    for version in &mut versions_with_paths {
129                        version.paths.clear();
130                    }
131                }
132
133                output.insert(language.to_string(), versions_with_paths);
134            }
135        }
136
137        if path {
138            let mut compilers = output.values().flatten();
139            let Some(compiler) = compilers.next() else {
140                eyre::bail!("no compiler resolved");
141            };
142            eyre::ensure!(
143                compilers.next().is_none(),
144                "multiple compilers resolved; use `forge compiler resolve` to inspect them"
145            );
146
147            let path = resolved_compiler_path(&project, compiler)?;
148            if shell::is_json() {
149                sh_println!("{}", serde_json::to_string(&path)?)?;
150            } else {
151                sh_println!("{}", path.display())?;
152            }
153            return Ok(());
154        }
155
156        if shell::is_json() {
157            sh_println!("{}", serde_json::to_string(&output)?)?;
158            return Ok(());
159        }
160
161        for (language, compilers) in &output {
162            match shell::verbosity() {
163                0 => sh_println!("{language}:")?,
164                _ => sh_println!("{language}:\n")?,
165            }
166
167            for resolved_compiler in compilers {
168                let version = &resolved_compiler.version;
169                match shell::verbosity() {
170                    0 => sh_println!("- {version}")?,
171                    _ => {
172                        if let Some(evm) = &resolved_compiler.evm_version {
173                            sh_println!("{version} (<= {evm}):")?
174                        } else {
175                            sh_println!("{version}:")?
176                        }
177                    }
178                }
179
180                if shell::verbosity() > 0 {
181                    let paths = &resolved_compiler.paths;
182                    for (idx, path) in paths.iter().enumerate() {
183                        if idx == paths.len() - 1 {
184                            sh_println!("└── {path}\n")?
185                        } else {
186                            sh_println!("├── {path}")?
187                        }
188                    }
189                }
190            }
191
192            if shell::verbosity() == 0 {
193                sh_println!()?
194            }
195        }
196
197        Ok(())
198    }
199}
200
201fn resolved_compiler_path(
202    project: &Project<MultiCompiler>,
203    compiler: &ResolvedCompiler,
204) -> Result<PathBuf> {
205    match compiler.language {
206        MultiCompilerLanguage::Solc(_) => {
207            let solc = project
208                .compiler
209                .solc
210                .as_ref()
211                .ok_or_else(|| eyre::eyre!("Solidity compiler is not available"))?;
212            match solc {
213                SolcCompiler::AutoDetect => Solc::find_svm_installed_version(&compiler.version)?
214                    .map(|solc| solc.solc)
215                    .ok_or_else(|| {
216                        eyre::eyre!("Solidity compiler {} is not installed", compiler.version)
217                    }),
218                SolcCompiler::Specific(solc) => Ok(solc.solc.clone()),
219            }
220        }
221        MultiCompilerLanguage::Vyper(_) => project
222            .compiler
223            .vyper
224            .as_ref()
225            .map(|vyper| vyper.path.clone())
226            .ok_or_else(|| eyre::eyre!("Vyper compiler is not available")),
227    }
228}