forge/cmd/
compiler.rs

1use clap::{Parser, Subcommand, ValueHint};
2use eyre::Result;
3use foundry_common::shell;
4use foundry_compilers::{artifacts::EvmVersion, Graph};
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                        .iter()
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                                if regex.is_match(&path_str) {
87                                    return None;
88                                }
89                            }
90
91                            Some(path_str)
92                        })
93                        .collect();
94
95                    let evm_version = if shell::verbosity() > 1 {
96                        Some(
97                            EvmVersion::default()
98                                .normalize_version_solc(version)
99                                .unwrap_or_default(),
100                        )
101                    } else {
102                        None
103                    };
104
105                    ResolvedCompiler { version: version.clone(), evm_version, paths }
106                })
107                .filter(|version| !version.paths.is_empty())
108                .collect();
109
110            // Sort by SemVer version.
111            versions_with_paths.sort_by(|v1, v2| Version::cmp(&v1.version, &v2.version));
112
113            // Skip language if no paths are found after filtering.
114            if !versions_with_paths.is_empty() {
115                // Clear paths if verbosity is 0, performed only after filtering to avoid being
116                // skipped.
117                if shell::verbosity() == 0 {
118                    versions_with_paths.iter_mut().for_each(|version| version.paths.clear());
119                }
120
121                output.insert(language.to_string(), versions_with_paths);
122            }
123        }
124
125        if shell::is_json() {
126            sh_println!("{}", serde_json::to_string(&output)?)?;
127            return Ok(());
128        }
129
130        for (language, compilers) in &output {
131            match shell::verbosity() {
132                0 => sh_println!("{language}:")?,
133                _ => sh_println!("{language}:\n")?,
134            }
135
136            for resolved_compiler in compilers {
137                let version = &resolved_compiler.version;
138                match shell::verbosity() {
139                    0 => sh_println!("- {version}")?,
140                    _ => {
141                        if let Some(evm) = &resolved_compiler.evm_version {
142                            sh_println!("{version} (<= {evm}):")?
143                        } else {
144                            sh_println!("{version}:")?
145                        }
146                    }
147                }
148
149                if shell::verbosity() > 0 {
150                    let paths = &resolved_compiler.paths;
151                    for (idx, path) in paths.iter().enumerate() {
152                        if idx == paths.len() - 1 {
153                            sh_println!("└── {path}\n")?
154                        } else {
155                            sh_println!("├── {path}")?
156                        }
157                    }
158                }
159            }
160
161            if shell::verbosity() == 0 {
162                sh_println!()?
163            }
164        }
165
166        Ok(())
167    }
168}