1use std::{
2 fmt, io, ops,
3 path::PathBuf,
4 process::{ExitCode, ExitStatus, Termination},
5};
6
7use binstalk_downloader::{download::DownloadError, remote::Error as RemoteError};
8use binstalk_fetchers::FetchError;
9use binstalk_types::cargo_toml_binstall::TargetTripleParseError;
10use compact_str::CompactString;
11use itertools::Itertools;
12use miette::{Diagnostic, Report};
13use thiserror::Error;
14use tokio::task;
15
16use crate::{
17 bins,
18 helpers::{
19 cargo_toml::Error as CargoTomlError,
20 cargo_toml_workspace::Error as LoadManifestFromWSError, gh_api_client::GhApiError,
21 },
22 registry::{InvalidRegistryError, RegistryError},
23};
24
25#[derive(Debug, Error)]
26#[error("version string '{v}' is not semver: {err}")]
27pub struct VersionParseError {
28 pub v: CompactString,
29 #[source]
30 pub err: semver::Error,
31}
32
33#[derive(Debug, Diagnostic, Error)]
34#[error("For crate {crate_name}: {err}")]
35pub struct CrateContextError {
36 crate_name: CompactString,
37 #[source]
38 #[diagnostic(transparent)]
39 err: BinstallError,
40}
41
42#[derive(Debug)]
43pub struct CrateErrors(Box<[Box<CrateContextError>]>);
44
45impl CrateErrors {
46 fn iter(&self) -> impl Iterator<Item = &CrateContextError> + Clone {
47 self.0.iter().map(ops::Deref::deref)
48 }
49
50 fn get_iter_for<'a, T: 'a>(
51 &'a self,
52 f: fn(&'a CrateContextError) -> Option<T>,
53 ) -> Option<impl Iterator<Item = T> + 'a> {
54 let iter = self.iter().filter_map(f);
55
56 if iter.clone().next().is_none() {
57 None
58 } else {
59 Some(iter)
60 }
61 }
62}
63
64impl fmt::Display for CrateErrors {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 fmt::Display::fmt(&self.0.iter().format(", "), f)
67 }
68}
69
70impl std::error::Error for CrateErrors {
71 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
72 self.0.first().map(|e| e as _)
73 }
74}
75
76impl miette::Diagnostic for CrateErrors {
77 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
78 Some(Box::new("binstall::many_failure"))
79 }
80
81 fn severity(&self) -> Option<miette::Severity> {
82 self.iter().filter_map(miette::Diagnostic::severity).max()
83 }
84
85 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
86 Some(Box::new(
87 self.get_iter_for(miette::Diagnostic::help)?.format("\n"),
88 ))
89 }
90
91 fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
92 Some(Box::new(
93 self.get_iter_for(miette::Diagnostic::url)?.format("\n"),
94 ))
95 }
96
97 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
98 self.iter().find_map(miette::Diagnostic::source_code)
99 }
100
101 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
102 let get_iter = || self.iter().filter_map(miette::Diagnostic::labels).flatten();
103
104 if get_iter().next().is_none() {
105 None
106 } else {
107 Some(Box::new(get_iter()))
108 }
109 }
110
111 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
112 Some(Box::new(
113 self.iter().map(|e| e as _).chain(
114 self.iter()
115 .filter_map(miette::Diagnostic::related)
116 .flatten(),
117 ),
118 ))
119 }
120
121 fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> {
122 self.0.first().map(|err| &**err as _)
123 }
124}
125
126#[derive(Debug, Error)]
127#[error("Invalid pkg-url {pkg_url} for {crate_name}@{version} on {target}: {reason}")]
128pub struct InvalidPkgFmtError {
129 pub crate_name: CompactString,
130 pub version: CompactString,
131 pub target: String,
132 pub pkg_url: String,
133 pub reason: &'static str,
134}
135
136#[derive(Error, Diagnostic, Debug)]
138#[non_exhaustive]
139pub enum BinstallError {
140 #[error(transparent)]
145 #[diagnostic(severity(error), code(binstall::internal::task_join))]
146 TaskJoinError(#[from] task::JoinError),
147
148 #[error("installation cancelled by user")]
155 #[diagnostic(severity(info), code(binstall::user_abort))]
156 UserAbort,
157
158 #[error("Crate {crate_name} is signed and package {package_name} failed verification")]
163 #[diagnostic(severity(error), code(binstall::signature::invalid))]
164 InvalidSignature {
165 crate_name: CompactString,
166 package_name: CompactString,
167 },
168
169 #[error("Crate {0} does not have signing information")]
174 #[diagnostic(severity(error), code(binstall::signature::missing))]
175 MissingSignature(CompactString),
176
177 #[error("Failed to parse url: {0}")]
184 #[diagnostic(severity(error), code(binstall::url_parse))]
185 UrlParse(#[from] url::ParseError),
186
187 #[error(transparent)]
192 #[diagnostic(severity(error), code(binstall::template))]
193 #[source_code(transparent)]
194 #[label(transparent)]
195 TemplateParseError(
196 #[from]
197 #[diagnostic_source]
198 leon::ParseError,
199 ),
200
201 #[error(transparent)]
206 #[diagnostic(severity(error), code(binstall::fetch))]
207 #[source_code(transparent)]
208 #[label(transparent)]
209 FetchError(Box<FetchError>),
210
211 #[error(transparent)]
216 #[diagnostic(severity(error), code(binstall::download))]
217 Download(#[from] DownloadError),
218
219 #[error("subprocess {command} errored with {status}")]
226 #[diagnostic(severity(error), code(binstall::subprocess))]
227 SubProcess {
228 command: Box<str>,
229 status: ExitStatus,
230 },
231
232 #[error("I/O Error: {0}")]
237 #[diagnostic(severity(error), code(binstall::io))]
238 Io(io::Error),
239
240 #[error("Unknown registry name {0}, env `CARGO_REGISTRIES_{0}_INDEX` nor is it in .cargo/config.toml")]
245 #[diagnostic(severity(error), code(binstall::cargo_registry))]
246 UnknownRegistryName(CompactString),
247
248 #[error(transparent)]
255 #[diagnostic(transparent)]
256 RegistryError(#[from] Box<RegistryError>),
257
258 #[error("the --manifest-path is invalid or cannot be resolved")]
263 #[diagnostic(severity(error), code(binstall::cargo_manifest_path))]
264 CargoManifestPath,
265
266 #[error("Failed to parse cargo manifest: {0}")]
275 #[diagnostic(
276 severity(error),
277 code(binstall::cargo_manifest),
278 help("If you used --manifest-path, check the Cargo.toml syntax.")
279 )]
280 CargoManifest(Box<CargoTomlError>),
281
282 #[error(transparent)]
287 #[diagnostic(severity(error), code(binstall::cargo_registry))]
288 RegistryParseError(#[from] Box<InvalidRegistryError>),
289
290 #[error(transparent)]
298 #[diagnostic(severity(error), code(binstall::version::parse))]
299 VersionParse(#[from] Box<VersionParseError>),
300
301 #[error("superfluous version specification")]
308 #[diagnostic(
309 severity(error),
310 code(binstall::conflict::version),
311 help("You cannot use both crate@version and the --version option. Remove one.")
312 )]
313 SuperfluousVersionOption,
314
315 #[error("no binaries specified nor inferred")]
324 #[diagnostic(
325 severity(error),
326 code(binstall::resolve::binaries),
327 help("This crate doesn't specify any binaries, so there's nothing to install.")
328 )]
329 UnspecifiedBinaries,
330
331 #[error("failed to discovered a viable target from the host")]
342 #[diagnostic(
343 severity(error),
344 code(binstall::targets::none_host),
345 help("Try to specify --target")
346 )]
347 NoViableTargets,
348
349 #[error("failed to find or install binaries: {0}")]
354 #[diagnostic(
355 severity(error),
356 code(binstall::targets::none_host),
357 help("Try to specify --target")
358 )]
359 BinFile(#[from] bins::Error),
360
361 #[error("Cargo.toml of crate {0} does not have section \"Package\"")]
366 #[diagnostic(severity(error), code(binstall::cargo_manifest))]
367 CargoTomlMissingPackage(CompactString),
368
369 #[error("bin-dir configuration provided generates duplicate source path: {path}")]
374 #[diagnostic(severity(error), code(binstall::SourceFilePath))]
375 DuplicateSourceFilePath { path: PathBuf },
376
377 #[error("Fallback to cargo-install is disabled")]
382 #[diagnostic(severity(error), code(binstall::no_fallback_to_cargo_install))]
383 NoFallbackToCargoInstall,
384
385 #[error(transparent)]
390 #[diagnostic(severity(error), code(binstall::invalid_pkg_fmt))]
391 InvalidPkgFmt(Box<InvalidPkgFmtError>),
392
393 #[error("Request to GitHub API failed: {0}")]
398 #[diagnostic(severity(error), code(binstall::gh_api_failure))]
399 GhApiErr(#[source] Box<GhApiError>),
400
401 #[error("Failed to parse target triple: {0}")]
406 #[diagnostic(severity(error), code(binstall::target_triple_parse_error))]
407 TargetTripleParseError(#[source] Box<TargetTripleParseError>),
408
409 #[cfg(feature = "git")]
414 #[error("Failed to shallow clone git repository: {0}")]
415 #[diagnostic(severity(error), code(binstall::git))]
416 GitError(#[from] crate::helpers::git::GitError),
417
418 #[error(transparent)]
423 #[diagnostic(severity(error), code(binstall::load_manifest_from_workspace))]
424 LoadManifestFromWSError(#[from] Box<LoadManifestFromWSError>),
425
426 #[error("cargo-install does not support `--install-path`")]
431 #[diagnostic(
432 severity(error),
433 code(binatall::cargo_install_does_not_support_install_path)
434 )]
435 CargoInstallDoesNotSupportInstallPath,
436
437 #[error(transparent)]
439 #[diagnostic(transparent)]
440 CrateContext(Box<CrateContextError>),
441
442 #[error(transparent)]
444 #[diagnostic(transparent)]
445 Errors(CrateErrors),
446}
447
448impl BinstallError {
449 fn exit_number(&self) -> u8 {
450 use BinstallError::*;
451 let code: u8 = match self {
452 TaskJoinError(_) => 17,
453 UserAbort => 32,
454 InvalidSignature { .. } => 40,
455 MissingSignature(_) => 41,
456 UrlParse(_) => 65,
457 TemplateParseError(..) => 67,
458 FetchError(..) => 68,
459 Download(_) => 68,
460 SubProcess { .. } => 70,
461 Io(_) => 74,
462 UnknownRegistryName(_) => 75,
463 RegistryError { .. } => 76,
464 CargoManifestPath => 77,
465 CargoManifest { .. } => 78,
466 RegistryParseError(..) => 79,
467 VersionParse { .. } => 80,
468 SuperfluousVersionOption => 84,
469 UnspecifiedBinaries => 86,
470 NoViableTargets => 87,
471 BinFile(_) => 88,
472 CargoTomlMissingPackage(_) => 89,
473 DuplicateSourceFilePath { .. } => 90,
474 NoFallbackToCargoInstall => 94,
475 InvalidPkgFmt(..) => 95,
476 GhApiErr(..) => 96,
477 TargetTripleParseError(..) => 97,
478 #[cfg(feature = "git")]
479 GitError(_) => 98,
480 LoadManifestFromWSError(_) => 99,
481 CargoInstallDoesNotSupportInstallPath => 100,
482 CrateContext(context) => context.err.exit_number(),
483 Errors(errors) => (errors.0)[0].err.exit_number(),
484 };
485
486 debug_assert!(code != 64 && code != 16 && code != 1 && code != 2 && code != 0);
488
489 code
490 }
491
492 pub fn exit_code(&self) -> ExitCode {
500 self.exit_number().into()
501 }
502
503 pub fn crate_context(self, crate_name: impl Into<CompactString>) -> Self {
505 self.crate_context_inner(crate_name.into())
506 }
507
508 fn crate_context_inner(self, crate_name: CompactString) -> Self {
509 match self {
510 Self::CrateContext(mut crate_context_error) => {
511 crate_context_error.crate_name = crate_name;
512 Self::CrateContext(crate_context_error)
513 }
514 err => Self::CrateContext(Box::new(CrateContextError { err, crate_name })),
515 }
516 }
517
518 pub fn crate_errors(mut errors: Vec<Box<CrateContextError>>) -> Option<Self> {
519 if errors.is_empty() {
520 None
521 } else if errors.len() == 1 {
522 Some(Self::CrateContext(errors.pop().unwrap()))
523 } else {
524 Some(Self::Errors(CrateErrors(errors.into_boxed_slice())))
525 }
526 }
527
528 pub fn get_report(self) -> Option<Report> {
529 if let BinstallError::UserAbort = self {
530 None
531 } else {
532 Some(Report::new(self))
533 }
534 }
535}
536
537impl Termination for BinstallError {
538 fn report(self) -> ExitCode {
539 self.exit_code()
540 }
541}
542
543impl From<io::Error> for BinstallError {
544 fn from(err: io::Error) -> Self {
545 err.downcast::<BinstallError>()
546 .unwrap_or_else(BinstallError::Io)
547 }
548}
549
550impl From<BinstallError> for io::Error {
551 fn from(e: BinstallError) -> io::Error {
552 match e {
553 BinstallError::Io(io_error) => io_error,
554 e => io::Error::other(e),
555 }
556 }
557}
558
559impl From<RemoteError> for BinstallError {
560 fn from(e: RemoteError) -> Self {
561 DownloadError::from(e).into()
562 }
563}
564
565impl From<CargoTomlError> for BinstallError {
566 fn from(e: CargoTomlError) -> Self {
567 BinstallError::CargoManifest(Box::new(e))
568 }
569}
570
571impl From<InvalidPkgFmtError> for BinstallError {
572 fn from(e: InvalidPkgFmtError) -> Self {
573 BinstallError::InvalidPkgFmt(Box::new(e))
574 }
575}
576
577impl From<GhApiError> for BinstallError {
578 fn from(e: GhApiError) -> Self {
579 BinstallError::GhApiErr(Box::new(e))
580 }
581}
582
583impl From<TargetTripleParseError> for BinstallError {
584 fn from(e: TargetTripleParseError) -> Self {
585 BinstallError::TargetTripleParseError(Box::new(e))
586 }
587}
588
589impl From<RegistryError> for BinstallError {
590 fn from(e: RegistryError) -> Self {
591 BinstallError::RegistryError(Box::new(e))
592 }
593}
594
595impl From<InvalidRegistryError> for BinstallError {
596 fn from(e: InvalidRegistryError) -> Self {
597 BinstallError::RegistryParseError(Box::new(e))
598 }
599}
600
601impl From<LoadManifestFromWSError> for BinstallError {
602 fn from(e: LoadManifestFromWSError) -> Self {
603 BinstallError::LoadManifestFromWSError(Box::new(e))
604 }
605}
606
607impl From<FetchError> for BinstallError {
608 fn from(e: FetchError) -> Self {
609 BinstallError::FetchError(Box::new(e))
610 }
611}