Skip to main content

forge/cmd/
compiler.rs

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