Skip to main content

foundry_cli/opts/
global.rs

1use clap::{ArgAction, Parser};
2use foundry_common::{
3    shell::{ColorChoice, OutputFormat, OutputMode, Shell, Verbosity},
4    version::{IS_NIGHTLY_VERSION, NIGHTLY_VERSION_WARNING_MESSAGE},
5};
6use foundry_compilers::error::{Result as CompilerResult, SolcError};
7use foundry_config::{Config, figment::Profile};
8use serde::{Deserialize, Serialize};
9use std::{
10    io::{self, IsTerminal, Write},
11    path::{Path, PathBuf},
12    sync::Mutex,
13};
14
15static LOCAL_COMPILER_APPROVALS: Mutex<Vec<(PathBuf, bool)>> = Mutex::new(Vec::new());
16
17fn ensure_local_compiler_approved(path: &Path) -> CompilerResult<()> {
18    let mut approvals = LOCAL_COMPILER_APPROVALS.lock().unwrap_or_else(|err| err.into_inner());
19    if let Some((_, approved)) = approvals.iter().find(|(approved_path, _)| approved_path == path) {
20        return if *approved { Ok(()) } else { Err(local_compiler_not_approved(path)) };
21    }
22
23    if !io::stdin().is_terminal() || !io::stderr().is_terminal() {
24        return Err(local_compiler_not_approved(path));
25    }
26
27    let mut stderr = io::stderr().lock();
28    writeln!(
29        stderr,
30        "Warning: this project is configured to use a local compiler executable:\n  {path:?}\n\
31         Running this executable may execute arbitrary code.",
32    )
33    .map_err(|err| SolcError::msg(format!("failed to write compiler approval prompt: {err}")))?;
34    write!(stderr, "Do you trust this compiler and want to continue? [y/N] ")
35        .and_then(|_| stderr.flush())
36        .map_err(|err| {
37            SolcError::msg(format!("failed to write compiler approval prompt: {err}"))
38        })?;
39
40    let mut response = String::new();
41    io::stdin()
42        .read_line(&mut response)
43        .map_err(|err| SolcError::msg(format!("failed to read compiler approval: {err}")))?;
44    let approved = matches!(response.trim().to_ascii_lowercase().as_str(), "y" | "yes");
45    approvals.push((path.to_path_buf(), approved));
46
47    if approved { Ok(()) } else { Err(local_compiler_not_approved(path)) }
48}
49
50fn local_compiler_not_approved(path: &Path) -> SolcError {
51    SolcError::msg(format!(
52        "refusing to run unapproved local compiler {path:?}; pass `--allow-local-compiler` if you trust this executable"
53    ))
54}
55
56/// Global arguments for the CLI.
57#[derive(Clone, Debug, Default, Serialize, Deserialize, Parser)]
58pub struct GlobalArgs {
59    /// Verbosity level of the log messages.
60    ///
61    /// Pass multiple times to increase the verbosity (e.g. -v, -vv, -vvv).
62    ///
63    /// Depending on the context the verbosity levels have different meanings.
64    ///
65    /// For example, the verbosity levels of the EVM are:
66    /// - 2 (-vv): Print logs for all tests.
67    /// - 3 (-vvv): Print execution traces for failing tests.
68    /// - 4 (-vvvv): Print execution traces for all tests, and setup traces for failing tests.
69    /// - 5 (-vvvvv): Print execution and setup traces for all tests, including storage changes and
70    ///   backtraces with line numbers.
71    #[arg(help_heading = "Display options", global = true, short, long, verbatim_doc_comment, conflicts_with = "quiet", action = ArgAction::Count)]
72    verbosity: Verbosity,
73
74    /// Do not print log messages.
75    #[arg(help_heading = "Display options", global = true, short, long, alias = "silent")]
76    quiet: bool,
77
78    /// Format log messages as JSON.
79    #[arg(help_heading = "Display options", global = true, long, alias = "format-json", conflicts_with_all = &["quiet", "color"])]
80    json: bool,
81
82    /// Format log messages as Markdown.
83    #[arg(
84        help_heading = "Display options",
85        global = true,
86        long,
87        alias = "markdown",
88        conflicts_with = "json"
89    )]
90    md: bool,
91
92    /// The color of the log messages.
93    #[arg(help_heading = "Display options", global = true, long, value_enum)]
94    color: Option<ColorChoice>,
95
96    /// Number of threads to use. Specifying 0 defaults to the number of logical cores.
97    #[arg(global = true, long, short = 'j', visible_alias = "jobs")]
98    threads: Option<usize>,
99
100    /// The configuration profile to use.
101    #[arg(global = true, long, value_name = "PROFILE")]
102    profile: Option<Profile>,
103
104    /// Allow use of local compiler executables without prompting.
105    #[arg(global = true, long, help_heading = "Compiler options")]
106    allow_local_compiler: bool,
107
108    /// Allow loading project dotenv files without prompting.
109    #[arg(global = true, long, help_heading = "Project options")]
110    allow_project_env: bool,
111}
112
113impl GlobalArgs {
114    /// Check if `--markdown-help` was passed and print CLI reference as Markdown, then exit.
115    ///
116    /// This must be called **before** parsing arguments, since commands with required
117    /// subcommands would fail parsing before the flag is checked.
118    pub fn check_markdown_help<C: clap::CommandFactory>() {
119        if std::env::args().take_while(|a| a != "--").any(|a| a == "--markdown-help") {
120            // Pre-parse: `Shell` is not initialized yet, so `sh_*` is unavailable.
121            foundry_cli_markdown::print_help_markdown::<C>();
122            std::process::exit(0);
123        }
124    }
125
126    /// Initialize the global options.
127    pub fn init(&self) -> eyre::Result<()> {
128        if let Some(profile) = &self.profile
129            && let Err(selected) = Config::try_set_selected_profile(profile.clone())
130        {
131            eyre::bail!(
132                "configuration profile was already initialized as `{selected}`, cannot select \
133                 `{profile}`"
134            );
135        }
136        let _ = Config::selected_profile();
137
138        let allow_local_compiler = self.allow_local_compiler;
139        foundry_compilers::set_compiler_approval_handler(move |path| {
140            if allow_local_compiler { Ok(()) } else { ensure_local_compiler_approved(path) }
141        });
142
143        // Set the global shell.
144        let shell = self.shell();
145        // Argument takes precedence over the env var global color choice.
146        match shell.color_choice() {
147            ColorChoice::Auto => {}
148            ColorChoice::Always => yansi::enable(),
149            ColorChoice::Never => yansi::disable(),
150        }
151        shell.set();
152
153        // Initialize the thread pool only if `threads` was requested to avoid unnecessary overhead.
154        if self.threads.is_some() {
155            self.force_init_thread_pool()?;
156        }
157
158        // Display a warning message if the current version is not stable.
159        if IS_NIGHTLY_VERSION
160            && !self.json
161            && std::env::var_os("FOUNDRY_DISABLE_NIGHTLY_WARNING").is_none()
162        {
163            let _ = sh_warn!("{}", NIGHTLY_VERSION_WARNING_MESSAGE);
164        }
165
166        Ok(())
167    }
168
169    /// Create a new shell instance.
170    pub fn shell(&self) -> Shell {
171        let mode = match self.quiet {
172            true => OutputMode::Quiet,
173            false => OutputMode::Normal,
174        };
175        let color = self.json.then_some(ColorChoice::Never).or(self.color).unwrap_or_default();
176        let format = if self.json {
177            OutputFormat::Json
178        } else if self.md {
179            OutputFormat::Markdown
180        } else {
181            OutputFormat::Text
182        };
183
184        Shell::new_with(format, mode, color, self.verbosity)
185    }
186
187    /// Initialize the global thread pool.
188    pub fn force_init_thread_pool(&self) -> eyre::Result<()> {
189        init_thread_pool(self.threads.unwrap_or(0))
190    }
191
192    /// Creates a new tokio runtime.
193    #[track_caller]
194    pub fn tokio_runtime(&self) -> tokio::runtime::Runtime {
195        let mut builder = tokio::runtime::Builder::new_multi_thread();
196        if let Some(threads) = self.threads
197            && threads > 0
198        {
199            builder.worker_threads(threads);
200        }
201        builder.enable_all().build().expect("failed to create tokio runtime")
202    }
203
204    /// Creates a new tokio runtime and blocks on the future.
205    #[track_caller]
206    pub fn block_on<F: std::future::Future>(&self, future: F) -> F::Output {
207        self.tokio_runtime().block_on(future)
208    }
209}
210
211/// Initialize the global thread pool.
212pub fn init_thread_pool(threads: usize) -> eyre::Result<()> {
213    rayon::ThreadPoolBuilder::new()
214        .thread_name(|i| format!("foundry-{i}"))
215        .num_threads(threads)
216        .build_global()?;
217    Ok(())
218}