1use 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 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
297pub 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 pub fn host_bin_dir(&self) -> PathBuf {
324 self.path.join("bin")
325 }
326
327 pub fn lib_dir(&self) -> PathBuf {
331 self.path.join("lib")
332 }
333
334 pub fn multilib_dir(&self) -> PathBuf {
341 self.path.join("lib").join("clang-runtimes")
342 }
343
344 pub fn target_lib_dir(&self, triple: &str, variant: &str) -> PathBuf {
360 self.multilib_dir().join(triple).join(variant).join("lib")
361 }
362
363 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}