arm_toolchain/
cli.rs

1use std::{io, sync::LazyLock};
2
3use crate::toolchain::{ToolchainClient, ToolchainError, ToolchainVersion};
4use clap::builder::styling;
5use humansize::DECIMAL;
6use indicatif::ProgressStyle;
7use miette::Diagnostic;
8use thiserror::Error;
9use tokio_util::{future::FutureExt, sync::CancellationToken};
10
11#[derive(Debug, Error, Diagnostic)]
12pub enum CliError {
13    #[error(transparent)]
14    #[diagnostic(code(arm_toolchain::cli::interactive_prompt_failed))]
15    Inquire(#[from] inquire::InquireError),
16
17    #[error(transparent)]
18    #[diagnostic(transparent)]
19    Toolchain(ToolchainError),
20
21    #[error("No ARM toolchain is enabled on this system")]
22    #[diagnostic(code(arm_toolchain::cli::no_toolchain_enabled))]
23    #[diagnostic(help("Install and activate a toolchain by running the `use latest` subcommand."))]
24    NoToolchainEnabled,
25
26    #[error("The toolchain {:?} is not installed.", version.name)]
27    #[diagnostic(code(arm_toolchain::cli::toolchain_missing))]
28    #[diagnostic(help("Install and activate it by running the `install {version}` subcommand."))]
29    ToolchainNotInstalled { version: ToolchainVersion },
30
31    #[error("No ARM toolchains are installed on this system")]
32    #[diagnostic(code(arm_toolchain::cli::no_toolchains_installed))]
33    #[diagnostic(help("There is nothing to remove."))]
34    NoToolchainsToRemove,
35
36    #[error("The toolchain {:?} is not installed.", version.name)]
37    #[diagnostic(code(arm_toolchain::cli::remove_missing))]
38    CannotRemoveMissingToolchain { version: ToolchainVersion },
39}
40
41impl From<ToolchainError> for CliError {
42    fn from(value: ToolchainError) -> Self {
43        match value {
44            // CLI version has a different help message.
45            ToolchainError::ToolchainNotInstalled { version } => {
46                Self::ToolchainNotInstalled { version }
47            }
48            other => Self::Toolchain(other),
49        }
50    }
51}
52
53impl From<io::Error> for CliError {
54    fn from(value: io::Error) -> Self {
55        ToolchainError::from(value).into()
56    }
57}
58
59/// Arm Toolchain Manager is a tool for installing and managing the LLVM-based ARM embedded toolchain.
60///
61/// See also: `atrun`
62#[derive(Debug, clap::Subcommand)]
63pub enum ArmToolchainCmd {
64    /// Install, verify, and extract a version of the ARM Embedded Toolchain.
65    ///
66    /// Toolchains are installed per-user in a platform-specific data directory.
67    /// If there is another toolchain already installed, that toolchain will still
68    /// be used after installing this one.
69    ///
70    /// If you would like to enable a toolchain you've installed, or install and enable
71    /// a toolchain all at once, invoke the `use` command instead.
72    #[clap(
73        visible_alias("add"),
74        visible_alias("i"),
75    )]
76    Install(InstallArgs),
77    /// Uninstall a single toolchain version, or all versions.
78    ///
79    /// When a toolchain is uninstalled, it is unset as the current toolchain and deleted
80    /// from the toolchains directory and download cache.
81    ///
82    /// If "all" is specified as the version to remove, every toolchain on the system will be
83    /// uninstalled.
84    #[clap(
85        visible_alias("uninstall"),
86        visible_alias("rm"),
87    )]
88    Remove(RemoveArgs),
89    /// Run a command with the active toolchain added to the `PATH`.
90    ///
91    /// Unless you specify `--no-cross-env`, the `TARGET_CC` and `TARGET_AR` environment
92    /// variables will also be set to `clang` and `llvm-ar` respectively. These will resolve
93    /// to the toolchain's versions of clang and llvm-ar.
94    ///
95    /// An alias for this command is the external `atrun` executable. You may need to pass an
96    /// extra `--` to the command if some flags look like ones `arm-toolchain` would accept.
97    Run(RunArgs),
98    /// Print the path of the active toolchain.
99    #[clap(
100        visible_alias("which"),
101        visible_alias("where"),
102        visible_alias("print"),
103    )]
104    Locate(LocateArgs),
105    /// Active a desired version of the ARM Embedded Toolchain, downloading it if necessary.
106    #[clap(
107        visible_alias("set"),
108        visible_alias("activate"),
109    )]
110    Use(UseArgs),
111    /// List all installed toolchain versions and the current active version.
112    #[clap(visible_alias("ls"))]
113    List,
114    /// Delete the cache which stores incomplete downloads.
115    PurgeCache,
116}
117
118impl ArmToolchainCmd {
119    /// Run the command.
120    pub async fn run(self) -> Result<(), CliError> {
121        match self {
122            ArmToolchainCmd::Install(config) => {
123                install(config).await?;
124            }
125            ArmToolchainCmd::Remove(args) => {
126                remove(args).await?;
127            }
128            ArmToolchainCmd::Run(args) => {
129                run(args).await?;
130            }
131            ArmToolchainCmd::Locate(args) => {
132                locate(args).await?;
133            }
134            ArmToolchainCmd::Use(args) => {
135                use_cmd(args).await?;
136            }
137            ArmToolchainCmd::List => {
138                list().await?;
139            }
140            ArmToolchainCmd::PurgeCache => {
141                purge_cache().await?;
142            }
143        }
144
145        Ok(())
146    }
147}
148
149mod install;
150pub use install::*;
151
152mod run;
153pub use run::*;
154
155mod use_cmd;
156pub use use_cmd::*;
157
158mod remove;
159pub use remove::*;
160
161/// Options for locating a toolchain.
162#[derive(Debug, clap::Args)]
163pub struct LocateArgs {
164    /// The toolchain that should be located.
165    #[arg(short = 'T', long)]
166    toolchain: Option<ToolchainVersion>,
167    /// Which path should be displayed.
168    #[clap(default_value = "install-dir")]
169    what: LocateWhat,
170}
171
172#[derive(Debug, Clone, Default, PartialEq, clap::ValueEnum)]
173enum LocateWhat {
174    /// The root directory, where the toolchain is installed.
175    #[default]
176    InstallDir,
177    /// The `/bin` directory, where executables are stored (e.g. clang).
178    Bin,
179    /// The `/lib` directory, where libraries are stored (e.g. libLTO.dylib).
180    Lib,
181    /// The multilib directory, where cross-compilation libraries are stored
182    /// for various platforms (e.g. libc.a).
183    Multilib,
184}
185
186/// Locate a toolchain's path and print it to stdio.
187pub async fn locate(args: LocateArgs) -> Result<(), CliError> {
188    let client = ToolchainClient::using_data_dir().await?;
189    let version = args
190        .toolchain
191        .or_else(|| client.active_toolchain())
192        .ok_or(CliError::NoToolchainEnabled)?;
193
194    let toolchain = client.toolchain(&version).await?;
195
196    match args.what {
197        LocateWhat::InstallDir => {
198            println!("{}", toolchain.path.display());
199        }
200        LocateWhat::Bin => {
201            println!("{}", toolchain.host_bin_dir().display());
202        }
203        LocateWhat::Lib => {
204            println!("{}", toolchain.lib_dir().display());
205        }
206        LocateWhat::Multilib => {
207            println!("{}", toolchain.multilib_dir().display());
208        }
209    }
210
211    Ok(())
212}
213
214/// Print a list of all toolchains to stdio.
215pub async fn list() -> Result<(), CliError> {
216    let client = ToolchainClient::using_data_dir().await?;
217
218    let active = client.active_toolchain();
219    let installed = client.installed_versions().await?;
220
221    println!(
222        "Active: {}",
223        active
224            .map(|v| v.to_string())
225            .unwrap_or_else(|| "None".to_string())
226    );
227
228    println!();
229    println!("Installed:");
230
231    if installed.is_empty() {
232        println!("- (None)");
233    }
234
235    for version in installed {
236        println!("- {version}");
237    }
238
239    Ok(())
240}
241
242/// Purge the download cache and print results to stdio.
243pub async fn purge_cache() -> Result<(), CliError> {
244    let client = ToolchainClient::using_data_dir().await?;
245    let bytes = client.purge_cache().await?;
246
247    println!(
248        "ARM Toolchain download cache purged ({} deleted)",
249        humansize::format_size(bytes, DECIMAL)
250    );
251
252    Ok(())
253}
254
255macro_rules! msg {
256    ($label:expr, $($rest:tt)+) => {
257        {
258            use owo_colors::OwoColorize;
259            eprintln!("{:>12} {}", $label.green().bold(), format_args!($($rest)+))
260        }
261    };
262}
263pub(crate) use msg;
264
265/// Create a cancel token that will trigger when Ctrl-C (SIGINT on Unix) is pressed.
266///
267/// If the token is cancelled manually, Ctrl-C's behavior will return to exiting the
268/// process. It is advised to not call this function in a loop because it creates a
269/// Tokio task that only exits after Ctrl-C is pressed twice.
270pub fn ctrl_c_cancel() -> CancellationToken {
271    let cancel_token = CancellationToken::new();
272
273    tokio::spawn({
274        let cancel_token = cancel_token.clone();
275        async move {
276            if let Some(wait_result) = tokio::signal::ctrl_c()
277                .with_cancellation_token(&cancel_token)
278                .await
279            {
280                // If this resolved to Some, it means that ctrl-c was pressed
281                // before the cancel token was invoked through other means.
282                // So: tell the user and cancel the token.
283
284                wait_result.unwrap();
285                cancel_token.cancel();
286                eprintln!("Cancelled.");
287            }
288
289            tokio::signal::ctrl_c().await.unwrap();
290            std::process::exit(1);
291        }
292    });
293
294    cancel_token
295}
296
297const PROGRESS_CHARS: &str = "=> ";
298
299pub static PROGRESS_STYLE_DL: LazyLock<ProgressStyle> = LazyLock::new(|| {
300    ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.blue}] ({bytes}/{total_bytes}, {eta} remaining) {bytes_per_sec}")
301    .expect("progress style valid")
302    .progress_chars(PROGRESS_CHARS)
303});
304
305pub static PROGRESS_STYLE_DL_MSG: LazyLock<ProgressStyle> = LazyLock::new(|| {
306    ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.blue}] ({bytes}/{total_bytes}) {msg}")
307        .expect("progress style valid")
308        .progress_chars(PROGRESS_CHARS)
309});
310
311pub static PROGRESS_STYLE_VERIFY: LazyLock<ProgressStyle> = LazyLock::new(|| {
312    ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.green}] {msg} ({eta} remaining)")
313        .expect("progress style valid")
314        .progress_chars(PROGRESS_CHARS)
315});
316
317pub static PROGRESS_STYLE_EXTRACT_SPINNER: LazyLock<ProgressStyle> = LazyLock::new(|| {
318    ProgressStyle::with_template("{spinner:.green} {msg}")
319        .expect("progress style valid")
320        .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✓")
321});
322
323pub static PROGRESS_STYLE_EXTRACT: LazyLock<ProgressStyle> = LazyLock::new(|| {
324    ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.dim}] {msg} ({eta} remaining)")
325        .expect("progress style valid")
326        .progress_chars(PROGRESS_CHARS)
327});
328
329pub static PROGRESS_STYLE_DELETE_SPINNER: LazyLock<ProgressStyle> = LazyLock::new(|| {
330    ProgressStyle::with_template("{spinner:.red} {msg}")
331        .expect("progress style valid")
332        .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✓")
333});
334
335pub static PROGRESS_STYLE_DELETE: LazyLock<ProgressStyle> = LazyLock::new(|| {
336    ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.red}] {msg} ({eta} remaining)")
337        .expect("progress style valid")
338        .progress_chars(PROGRESS_CHARS)
339});
340
341pub const STYLES: styling::Styles = styling::Styles::styled()
342    .header(styling::AnsiColor::Green.on_default().bold())
343    .usage(styling::AnsiColor::Green.on_default().bold())
344    .literal(styling::AnsiColor::Blue.on_default().bold())
345    .placeholder(styling::AnsiColor::Cyan.on_default());