arm_toolchain/
toolchain.rs

1//! Install and manage the Arm Toolchain for Embedded (ATfE).
2//!
3//! The included [`ToolchainClient`] can be used to fetch the latest release from the Arm GitHub repository,
4//! download the appropriate asset for the current host OS and architecture, and install it to a specified
5//! directory. It also handles checksum verification and extraction of the downloaded archive.
6//!
7//! Toolchains are installed per-user in a platform-specific data directory managed by the library.
8//! It is recommended that you use the library's default data directory by creating a toolchain client via the
9//! [`ToolchainClient::using_data_dir`] function.
10//!
11//! Once you've installed a toolchain, get a handle to it with [`ToolchainClient::toolchain`]. This will
12//! allow you to access information such as the filesystem directory where its executables are contained.
13
14use std::{
15    cell::OnceCell,
16    fmt::{self, Debug, Display},
17    path::PathBuf,
18    sync::Arc,
19};
20
21use miette::Diagnostic;
22use octocrab::models::repos::{Asset, Release};
23use strum::AsRefStr;
24use thiserror::Error;
25use tracing::{debug, error, trace};
26
27mod client;
28mod extract;
29mod remove;
30
31pub use client::*;
32pub use remove::RemoveProgress;
33
34static APP_USER_AGENT: &str = concat!(
35    "vexide/",
36    env!("CARGO_PKG_NAME"),
37    "@",
38    env!("CARGO_PKG_VERSION"),
39    " (",
40    env!("CARGO_PKG_REPOSITORY"),
41    ")",
42);
43
44#[derive(Debug, Error, Diagnostic)]
45pub enum ToolchainError {
46    #[error(
47        "Failed to determine the latest Arm Toolchain for Embedded version.\nCandidates:\n{}",
48        candidates.iter().map(|release| format!(" • {release}")).collect::<Vec<_>>().join("\n")
49    )]
50    #[diagnostic(code(arm_toolchain::toolchain::latest_release_not_found))]
51    LatestReleaseMissing { candidates: Vec<String> },
52    #[error(
53        "Failed to determine a compatible toolchain asset for {allowed_os:?} {}.\nCandidates:\n{}",
54        allowed_arches.iter().map(|a| a.as_ref()).collect::<Vec<_>>().join("/"),
55        candidates.iter().map(|release| format!(" • {release}")).collect::<Vec<_>>().join("\n")
56    )]
57    #[diagnostic(code(arm_toolchain::toolchain::release_asset_not_found))]
58    ReleaseAssetMissing {
59        allowed_os: HostOS,
60        allowed_arches: Vec<HostArch>,
61        candidates: Vec<String>,
62    },
63    #[error("Cannot download {name} because it has an invalid name")]
64    #[diagnostic(code(arm_toolchain::toolchain::invalid_asset_name))]
65    InvalidAssetName { name: String },
66
67    #[error(
68        "The checksum of the downloaded asset did not match the expected value.
69- Expected: {expected:?}
70- Actual: {actual:?}"
71    )]
72    #[diagnostic(code(arm_toolchain::toolchain::checksum_mismatch))]
73    #[diagnostic(help("the downloaded file may be corrupted or incomplete"))]
74    ChecksumMismatch { expected: String, actual: String },
75
76    #[error("Could not extract the toolchain asset")]
77    #[diagnostic(transparent)]
78    Extract(#[from] extract::ExtractError),
79
80    #[error("The toolchain installation was cancelled")]
81    #[diagnostic(code(arm_toolchain::toolchain::cancelled))]
82    Cancelled,
83
84    #[error("The toolchain {:?} is not installed.", version.name)]
85    #[diagnostic(code(arm_toolchain::toolchain::not_installed))]
86    ToolchainNotInstalled { version: ToolchainVersion },
87
88    #[error("A request to the GitHub API failed")]
89    #[diagnostic(code(arm_toolchain::toolchain::github_api))]
90    GitHubApi(#[from] octocrab::Error),
91    #[error("Failed to download the toolchain asset")]
92    #[diagnostic(code(arm_toolchain::toolchain::download_failed))]
93    Reqwest(#[from] reqwest::Error),
94    #[error("Failed to move a file to the trash")]
95    #[diagnostic(code(arm_toolchain::toolchain::trash_op_failed))]
96    Trash(#[from] trash::Error),
97    #[error(transparent)]
98    #[diagnostic(code(arm_toolchain::toolchain::io_error))]
99    Io(#[from] std::io::Error),
100}
101
102pub enum InstallState {
103    DownloadBegin { asset_size: u64, bytes_read: u64 },
104    Download { bytes_read: u64 },
105    DownloadFinish,
106
107    VerifyingBegin { asset_size: u64 },
108    Verifying { bytes_read: u64 },
109    VerifyingFinish,
110
111    ExtractBegin,
112    ExtractCopy { total_size: u64, bytes_copied: u64 },
113    ExtractCleanUp,
114    ExtractDone,
115}
116
117#[derive(Debug, AsRefStr, Clone, Copy)]
118pub enum HostOS {
119    Darwin,
120    Linux,
121    Windows,
122}
123
124impl HostOS {
125    pub const fn current() -> Self {
126        if cfg!(target_os = "macos") {
127            Self::Darwin
128        } else if cfg!(target_os = "linux") {
129            Self::Linux
130        } else if cfg!(windows) {
131            Self::Windows
132        } else {
133            panic!("This OS is not supported by the ARM toolchain")
134        }
135    }
136}
137
138#[derive(Debug, AsRefStr, Clone, Copy)]
139pub enum HostArch {
140    #[strum(serialize = "universal")]
141    Universal,
142    AAarch64,
143    #[strum(serialize = "x86_64")]
144    X86_64,
145}
146
147impl HostArch {
148    pub const fn current() -> &'static [Self] {
149        const ALLOWED_ARCHES: &[HostArch] = &[
150            #[cfg(target_arch = "x86_64")]
151            HostArch::X86_64,
152            #[cfg(target_arch = "aarch64")]
153            HostArch::AAarch64,
154            #[cfg(all(
155                target_os = "macos",
156                any(target_arch = "aarch64", target_arch = "x86_64")
157            ))]
158            HostArch::Universal,
159        ];
160
161        #[allow(clippy::const_is_empty)]
162        if ALLOWED_ARCHES.is_empty() {
163            panic!("This architecture is not supported by the ARM toolchain");
164        }
165
166        ALLOWED_ARCHES
167    }
168}
169
170pub struct ToolchainRelease {
171    release: Arc<Release>,
172    version: OnceCell<ToolchainVersion>,
173}
174
175impl ToolchainRelease {
176    const ALLOWED_EXTENSIONS: &[&str] = &["dmg", "tar.xz", "zip"];
177
178    pub fn new(release: Release) -> Self {
179        Self {
180            version: OnceCell::new(),
181            release: Arc::new(release),
182        }
183    }
184
185    pub fn version(&self) -> &ToolchainVersion {
186        self.version
187            .get_or_init(|| ToolchainVersion::from_tag_name(&self.release.tag_name))
188    }
189
190    pub fn asset_for(
191        &self,
192        os: HostOS,
193        allowed_arches: &[HostArch],
194    ) -> Result<&Asset, ToolchainError> {
195        debug!(
196            options = self.release.assets.len(),
197            ?os, ?allowed_arches, allowed_exts = ?Self::ALLOWED_EXTENSIONS,
198            "Searching for a compatible toolchain asset"
199        );
200
201        let asset = self
202            .release
203            .assets
204            .iter()
205            .find(|a| {
206                let mut components: Vec<&str> = a.name.split('-').collect();
207
208                // Remove the file extension from the last file name component
209                let last_idx = components.len() - 1;
210
211                let (last_component, file_extension) = components[last_idx]
212                    .split_once('.')
213                    .expect("filename has extension");
214                components[last_idx] = last_component;
215
216                let correct_os = components.contains(&os.as_ref());
217                let correct_arch = allowed_arches
218                    .iter()
219                    .any(|arch| components.contains(&arch.as_ref()));
220                let correct_extension = Self::ALLOWED_EXTENSIONS.contains(&file_extension);
221
222                let valid = correct_os && correct_arch && correct_extension;
223                trace!(
224                    name = a.name,
225                    correct_os, correct_arch, correct_extension, "Asset valid: {valid}"
226                );
227
228                valid
229            })
230            .ok_or_else(|| ToolchainError::ReleaseAssetMissing {
231                allowed_os: os,
232                allowed_arches: allowed_arches.to_vec(),
233                candidates: self
234                    .release
235                    .assets
236                    .iter()
237                    .map(|a| a.name.to_string())
238                    .collect(),
239            })?;
240
241        debug!(name = asset.name, "Found compatible asset");
242
243        Ok(asset)
244    }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct ToolchainVersion {
249    pub name: String,
250}
251
252impl ToolchainVersion {
253    pub fn named(name: impl Into<String>) -> Self {
254        Self { name: name.into() }
255    }
256
257    pub fn from_tag_name(tag_name: impl AsRef<str>) -> Self {
258        let mut name = tag_name.as_ref();
259        name = name
260            .strip_prefix(ToolchainClient::RELEASE_PREFIX)
261            .unwrap_or(name);
262        name = name
263            .strip_suffix(ToolchainClient::RELEASE_SUFFIX)
264            .unwrap_or(name);
265
266        Self {
267            name: name.to_string(),
268        }
269    }
270
271    fn to_tag_name(&self) -> String {
272        format!(
273            "{}{}{}",
274            ToolchainClient::RELEASE_PREFIX,
275            self.name,
276            ToolchainClient::RELEASE_SUFFIX
277        )
278    }
279}
280
281impl Display for ToolchainVersion {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        write!(f, "v{}", self.name)
284    }
285}
286
287impl From<&str> for ToolchainVersion {
288    fn from(mut version: &str) -> Self {
289        if let Some(bare) = version.strip_prefix("v") {
290            version = bare;
291        }
292
293        ToolchainVersion::named(version)
294    }
295}
296
297/// An ARM toolchain that may be installed on the current system.
298pub struct InstalledToolchain {
299    pub path: PathBuf,
300}
301
302impl InstalledToolchain {
303    pub fn new(path: PathBuf) -> Self {
304        Self { path }
305    }
306
307    pub async fn check_installed(&self) -> Result<(), ToolchainError> {
308        if !self.path.exists() {
309            return Err(ToolchainError::ToolchainNotInstalled {
310                version: ToolchainVersion::named(
311                    self.path.file_name().unwrap_or_default().to_string_lossy(),
312                ),
313            });
314        }
315
316        Ok(())
317    }
318
319    /// Returns the path to a directory containing binaries that run on the host.
320    ///
321    /// This directory typically contains the compiler (`clang`) and support executables
322    /// like `llvm-objcopy`.
323    pub fn host_bin_dir(&self) -> PathBuf {
324        self.path.join("bin")
325    }
326
327    /// Returns the path to a directory containing support libraries.
328    ///
329    /// This directory typically contains `libLTO.dylib`.
330    pub fn lib_dir(&self) -> PathBuf {
331        self.path.join("lib")
332    }
333
334    /// Returns the path to a directory containing a multilib.
335    ///
336    /// The path returned is equivalent to `self.lib_dir().join("clang-runtimes")`.
337    ///
338    /// This directory contains the libraries for all supported targets as well as a
339    /// `multilib.yaml` file which describes which sub-directories they are located in.
340    pub fn multilib_dir(&self) -> PathBuf {
341        self.path.join("lib").join("clang-runtimes")
342    }
343
344    /// Returns the path to a directory containing static libraries for the given target.
345    ///
346    /// Targets are considered to have both a triple and a variant. Non-library files
347    /// such as linker scripts or objects may be included in this directory.
348    ///
349    /// Example triples:
350    ///
351    /// - `arm-none-eabi`
352    /// - `aarch64-none-elf`
353    ///
354    /// Example variants:
355    ///
356    /// - `armv7a_soft_nofp` (ARMv7-A, soft float ABI, no FPU)
357    /// - `armv7m_soft_vfpv3_d16_exn_rtti` (ARMv7-M, soft float ABI, vfpv3 FPU, 16 float registers, with RTTI)
358    /// - `armv7a_soft_vfpv3_d16_exn_rtti` (ARMv7-M, soft float ABI, vfpv3 FPU, 16 float registers, with RTTI)
359    pub fn target_lib_dir(&self, triple: &str, variant: &str) -> PathBuf {
360        self.multilib_dir().join(triple).join(variant).join("lib")
361    }
362
363    /// Returns the paths to header directories for the given target.
364    ///
365    /// Targets are considered to have both a triple and a variant.
366    /// See [`Self::target_lib_dir`] for example triples and variants.
367    pub fn target_include_dirs(&self, triple: &str, variant: &str) -> Vec<PathBuf> {
368        let triple_dir = self.multilib_dir().join(triple);
369
370        vec![
371            triple_dir.join(variant).join("include"),
372            triple_dir.join("include"),
373        ]
374    }
375}