Skip to main content

forge/cmd/
compiler.rs

1use clap::{Parser, Subcommand, ValueHint};
2use eyre::Result;
3use foundry_common::shell;
4use foundry_compilers::{Graph, artifacts::EvmVersion};
5use foundry_config::Config;
6use semver::Version;
7use serde::Serialize;
8use std::{collections::BTreeMap, path::PathBuf};
9
10/// CLI arguments for `forge compiler`.
11#[derive(Debug, Parser)]
12pub struct CompilerArgs {
13    #[command(subcommand)]
14    pub sub: CompilerSubcommands,
15}
16
17impl CompilerArgs {
18    pub fn run(self) -> Result<()> {
19        match self.sub {
20            CompilerSubcommands::Resolve(args) => args.run(),
21        }
22    }
23}
24
25#[derive(Debug, Subcommand)]
26pub enum CompilerSubcommands {
27    /// Retrieves the resolved version(s) of the compiler within the project.
28    #[command(visible_alias = "r")]
29    Resolve(ResolveArgs),
30}
31
32/// Resolved compiler within the project.
33#[derive(Serialize)]
34struct ResolvedCompiler {
35    /// Compiler version.
36    version: Version,
37    /// Max supported EVM version of compiler.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    evm_version: Option<EvmVersion>,
40    /// Source paths.
41    #[serde(skip_serializing_if = "Vec::is_empty")]
42    paths: Vec<String>,
43}
44
45/// CLI arguments for `forge compiler resolve`.
46#[derive(Debug, Parser)]
47pub struct ResolveArgs {
48    /// The root directory
49    #[arg(long, short, value_hint = ValueHint::DirPath, value_name = "PATH")]
50    root: Option<PathBuf>,
51
52    /// Skip files that match the given regex pattern.
53    #[arg(long, short, value_name = "REGEX")]
54    skip: Option<regex::Regex>,
55}
56
57impl ResolveArgs {
58    pub fn run(self) -> Result<()> {
59        let Self { root, skip } = self;
60
61        let root = root.unwrap_or_else(|| PathBuf::from("."));
62        let config = Config::load_with_root(&root)?;
63        let project = config.project()?;
64
65        let graph = Graph::resolve(&project.paths)?;
66        let sources = graph.into_sources_by_version(&project)?.sources;
67
68        let mut output: BTreeMap<String, Vec<ResolvedCompiler>> = BTreeMap::new();
69
70        for (language, sources) in sources {
71            let mut versions_with_paths: Vec<ResolvedCompiler> = sources
72                .iter()
73                .map(|(version, sources, _)| {
74                    let paths: Vec<String> = sources
75                        .keys()
76                        .filter_map(|path_file| {
77                            let path_str = path_file
78                                .strip_prefix(&project.paths.root)
79                                .unwrap_or(path_file)
80                                .to_path_buf()
81                                .display()
82                                .to_string();
83
84                            // Skip files that match the given regex pattern.
85                            if let Some(ref regex) = skip
86                                && regex.is_match(&path_str)
87                            {
88                                return None;
89                            }
90
91                            Some(path_str)
92                        })
93                        .collect();
94
95                    let evm_version = (shell::verbosity() > 1).then(|| {
96                        EvmVersion::default().normalize_version_solc(version).unwrap_or_default()
97                    });
98
99                    ResolvedCompiler { version: version.clone(), evm_version, paths }
100                })
101                .filter(|version| !version.paths.is_empty())
102                .collect();
103
104            // Sort by SemVer version.
105            versions_with_paths.sort_by(|v1, v2| Version::cmp(&v1.version, &v2.version));
106
107            // Skip language if no paths are found after filtering.
108            if !versions_with_paths.is_empty() {
109                // Clear paths if verbosity is 0, performed only after filtering to avoid being
110                // skipped.
111                if shell::verbosity() == 0 {
112                    for version in &mut versions_with_paths {
113                        version.paths.clear();
114                    }
115                }
116
117                output.insert(language.to_string(), versions_with_paths);
118            }
119        }
120
121        if shell::is_json() {
122            sh_println!("{}", serde_json::to_string(&output)?)?;
123            return Ok(());
124        }
125
126        for (language, compilers) in &output {
127            match shell::verbosity() {
128                0 => sh_println!("{language}:")?,
129                _ => sh_println!("{language}:\n")?,
130            }
131
132            for resolved_compiler in compilers {
133                let version = &resolved_compiler.version;
134                match shell::verbosity() {
135                    0 => sh_println!("- {version}")?,
136                    _ => {
137                        if let Some(evm) = &resolved_compiler.evm_version {
138                            sh_println!("{version} (<= {evm}):")?
139                        } else {
140                            sh_println!("{version}:")?
141                        }
142                    }
143                }
144
145                if shell::verbosity() > 0 {
146                    let paths = &resolved_compiler.paths;
147                    for (idx, path) in paths.iter().enumerate() {
148                        if idx == paths.len() - 1 {
149                            sh_println!("└── {path}\n")?
150                        } else {
151                            sh_println!("├── {path}")?
152                        }
153                    }
154                }
155            }
156
157            if shell::verbosity() == 0 {
158                sh_println!()?
159            }
160        }
161
162        Ok(())
163    }
164}