1use crate::cli_options::{Backend, BackendName, MessageFormat};
2use crate::diagnostics::report::ReportCtx;
3use crate::prelude::*;
4
5#[derive_group(Serializers)]
7#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
8#[serde(rename_all = "snake_case")]
9pub enum ResolvedValue {
10 Version(String),
11 Path(String),
13}
14
15#[derive_group(Serializers)]
18#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
19pub struct ToolResolution {
20 pub name: String,
21 #[serde(flatten)]
22 pub resolved: ResolvedValue,
23 pub source: String,
24}
25
26impl ToolResolution {
27 fn value(&self) -> &str {
29 match &self.resolved {
30 ResolvedValue::Version(value) | ResolvedValue::Path(value) => value,
31 }
32 }
33}
34
35#[derive_group(Serializers)]
38#[derive(Debug, Clone, Copy, JsonSchema, Hash, Eq, PartialEq)]
39pub enum HaxLibCompatibility {
40 Compatible,
41 TooOld,
44 TooNew,
47}
48
49impl HaxLibCompatibility {
50 fn describe(self) -> &'static str {
52 match self {
53 Self::Compatible => "compatible",
54 Self::TooOld => "INCOMPATIBLE: too old for this cargo-hax",
55 Self::TooNew => "INCOMPATIBLE: newer than this cargo-hax",
56 }
57 }
58}
59
60#[derive_group(Serializers)]
62#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
63pub struct HaxLibStatus {
64 #[serde(rename = "crate")]
65 pub crate_name: String,
66 pub version: String,
67 pub compatibility: HaxLibCompatibility,
68}
69
70#[derive_group(Serializers)]
72#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
73pub struct MemberOverride {
74 #[serde(rename = "crate")]
75 pub crate_name: String,
76 pub tools: Vec<ToolResolution>,
77 pub versions: Vec<ToolResolution>,
78}
79
80#[derive_group(Serializers)]
82#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
83pub struct ToolVersionListing {
84 pub version: String,
85 pub installed: bool,
86 pub in_manifest: bool,
87 pub default: bool,
88 pub verified: bool,
91}
92
93#[derive_group(Serializers)]
95#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
96pub struct ToolListing {
97 pub tool: String,
98 pub versions: Vec<ToolVersionListing>,
99 pub omitted: usize,
101}
102
103#[derive_group(Serializers)]
105#[derive(Debug, Clone, Copy, JsonSchema, Hash, Eq, PartialEq)]
106pub enum InstallStatus {
107 Cached { verified: bool },
109 Installed { verified: bool },
111}
112
113#[derive_group(Serializers)]
115#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
116pub struct InstalledTool {
117 pub tool: String,
118 pub version: String,
119 pub status: InstallStatus,
120}
121
122#[derive_group(Serializers)]
124#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
125pub struct PinChange {
126 pub name: String,
127 pub version: String,
128 pub previous: Option<String>,
130}
131
132#[derive_group(Serializers)]
133#[derive(Debug, Clone, JsonSchema, Hash, Eq, PartialEq)]
134#[repr(u8)]
135pub enum HaxMessage {
136 Diagnostic {
137 diagnostic: super::Diagnostics,
138 working_dir: Option<PathBuf>,
139 } = 254,
140 BinaryNotFound {
141 binary_name: String,
142 env_var: String,
143 hint: Option<String>,
144 } = 0,
145 ProducedFile {
146 path: PathBuf,
147 wrote: bool,
148 } = 1,
149 HaxEngineFailure {
150 exit_code: i32,
151 } = 2,
152 CargoBuildFailure = 3,
153 WarnExperimentalBackend {
154 backend: Backend,
155 } = 4,
156 ProfilingData(crate::engine_api::ProfilingData) = 5,
157 Stats {
158 errors_per_item: Vec<(hax_frontend_exporter::DefId, usize)>,
159 } = 6,
160 GenericError {
161 message: String,
162 } = 7,
163 GenericWarning {
164 message: String,
165 } = 8,
166 Step {
167 verb: String,
168 target: String,
169 } = 9,
170 SubprocessOutput {
171 prefix: String,
172 line: String,
173 } = 10,
174 OutputTruncated {
175 prefix: String,
176 remaining: usize,
177 log_path: PathBuf,
178 } = 11,
179 UnsupportedOption {
180 option: String,
181 backend: BackendName,
182 } = 12,
183 HaxTomlWarning {
184 path: PathBuf,
185 message: String,
186 } = 13,
187 HaxTomlError {
188 path: PathBuf,
189 message: String,
190 } = 14,
191 MemberToolOverrides {
192 crate_name: String,
193 path: PathBuf,
194 entries: Vec<String>,
195 } = 15,
196 StrayHaxToml {
197 path: PathBuf,
198 } = 16,
199 UnverifiedInstall {
200 tool: String,
201 version: String,
202 url: String,
203 } = 17,
204 NonDefaultToolVersion {
205 tool: String,
206 used: String,
207 tested: String,
208 } = 18,
209 HaxLibIncompatible {
210 crate_name: String,
211 found: String,
212 binary: String,
213 expected: String,
214 newer: bool,
215 } = 19,
216 CachedUnverifiedToolInUse {
217 tool: String,
218 version: String,
219 } = 20,
220 ToolsShow {
222 tools: Vec<ToolResolution>,
224 versions: Vec<ToolResolution>,
226 hax_lib: Vec<HaxLibStatus>,
228 member_overrides: Vec<MemberOverride>,
229 } = 21,
230 ToolsList {
232 tools: Vec<ToolListing>,
233 installed_only: bool,
236 } = 22,
237 ToolsInstalled {
241 installed: Vec<InstalledTool>,
242 } = 23,
243 LakefilePinDrift {
246 path: PathBuf,
247 name: String,
250 found: String,
251 expected: String,
252 } = 24,
253 ToolRemoved {
256 tool: String,
257 version: String,
258 } = 25,
259 ToolsCleaned {
262 removed: usize,
263 } = 26,
264 ToolsPinned {
268 path: PathBuf,
269 changes: Vec<PinChange>,
270 skipped: Vec<String>,
271 } = 27,
272 RootModuleMissingImport {
275 path: PathBuf,
276 import: String,
277 } = 28,
278 RootModuleStaleImport {
281 path: PathBuf,
282 import: String,
283 } = 29,
284 ScenarioDryRun {
287 name: String,
288 package: String,
289 lines: Vec<String>,
291 } = 30,
292 ScenarioSummary {
294 total: usize,
295 failed: Vec<String>,
296 } = 31,
297}
298
299impl HaxMessage {
300 pub fn discriminant(&self) -> u16 {
302 unsafe { *(self as *const Self as *const u16) }
303 }
304
305 pub fn code(&self) -> String {
306 match self {
307 HaxMessage::Diagnostic { diagnostic, .. } => diagnostic.kind.code(),
308 _ => format!("CARGOHAX{:0>4}", self.discriminant()),
309 }
310 }
311}
312
313static ERROR_REPORTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
318
319pub fn errors_reported() -> bool {
321 ERROR_REPORTED.load(std::sync::atomic::Ordering::Relaxed)
322}
323
324const ENGINE_BINARY_NAME: &str = "hax-engine";
325
326use annotate_snippets::{Level, Renderer};
327
328fn warn_with_help(renderer: &Renderer, title: &str, remedy: &str) -> String {
330 format!(
331 "{}",
332 renderer.render(
333 Level::Warning
334 .title(title)
335 .footer(Level::Help.title(remedy))
336 )
337 )
338}
339
340fn relative_to_cwd(path: PathBuf) -> PathBuf {
342 std::env::current_dir()
343 .ok()
344 .and_then(|current_dir| path.strip_prefix(current_dir).ok())
345 .map(|relative| PathBuf::from(".").join(relative))
346 .unwrap_or(path)
347}
348
349impl HaxMessage {
350 pub fn is_error(&self) -> bool {
355 match self {
356 Self::Diagnostic { .. }
357 | Self::BinaryNotFound { .. }
358 | Self::HaxEngineFailure { .. }
359 | Self::GenericError { .. }
360 | Self::HaxTomlError { .. }
361 | Self::HaxLibIncompatible { .. } => true,
362 Self::ScenarioSummary { failed, .. } => !failed.is_empty(),
363 Self::ProducedFile { .. }
364 | Self::CargoBuildFailure
365 | Self::WarnExperimentalBackend { .. }
366 | Self::ProfilingData(..)
367 | Self::Stats { .. }
368 | Self::GenericWarning { .. }
369 | Self::Step { .. }
370 | Self::SubprocessOutput { .. }
371 | Self::OutputTruncated { .. }
372 | Self::UnsupportedOption { .. }
373 | Self::HaxTomlWarning { .. }
374 | Self::MemberToolOverrides { .. }
375 | Self::StrayHaxToml { .. }
376 | Self::UnverifiedInstall { .. }
377 | Self::NonDefaultToolVersion { .. }
378 | Self::CachedUnverifiedToolInUse { .. }
379 | Self::ToolsShow { .. }
380 | Self::ToolsList { .. }
381 | Self::ToolsInstalled { .. }
382 | Self::LakefilePinDrift { .. }
383 | Self::ToolRemoved { .. }
384 | Self::ToolsCleaned { .. }
385 | Self::ToolsPinned { .. }
386 | Self::RootModuleMissingImport { .. }
387 | Self::RootModuleStaleImport { .. }
388 | Self::ScenarioDryRun { .. } => false,
389 }
390 }
391
392 pub fn report(self, message_format: MessageFormat, rctx: Option<&mut ReportCtx>) {
393 if self.is_error() {
394 ERROR_REPORTED.store(true, std::sync::atomic::Ordering::Relaxed);
395 }
396 if let Some(rendered) = self.render(message_format, rctx)
399 && !rendered.is_empty()
400 {
401 println!("{rendered}")
402 }
403 }
404 pub fn report_styled(self, rctx: Option<&mut ReportCtx>) {
405 if self.is_error() {
406 ERROR_REPORTED.store(true, std::sync::atomic::Ordering::Relaxed);
407 }
408 println!("{}", self.render_styled(rctx))
409 }
410
411 pub fn render(
412 self,
413 message_format: MessageFormat,
414 mut rctx: Option<&mut ReportCtx>,
415 ) -> Option<String> {
416 if let (Some(r), HaxMessage::Diagnostic { diagnostic, .. }) = (rctx.as_mut(), &self)
417 && r.seen_already(diagnostic.clone())
418 {
419 return None;
420 }
421 Some(match message_format {
422 MessageFormat::Json => serde_json::to_string(&self).unwrap(),
423 MessageFormat::Human => self.render_styled(rctx),
424 })
425 }
426 pub fn render_styled(self, rctx: Option<&mut ReportCtx>) -> String {
427 let renderer = Renderer::styled();
428 match self {
429 Self::Diagnostic {
430 diagnostic,
431 working_dir,
432 } => {
433 let mut _rctx = None;
434 let rctx = rctx.unwrap_or_else(|| _rctx.get_or_insert(ReportCtx::default()));
435 diagnostic.with_message(
436 rctx,
437 working_dir.as_ref().map(PathBuf::as_path),
438 Level::Error,
439 |msg| format!("{}", renderer.render(msg)),
440 )
441 }
442 Self::BinaryNotFound {
443 binary_name,
444 env_var,
445 hint,
446 } => {
447 use colored::Colorize;
448 let mut message = format!(
449 "hax: The binary [{}] was not found in your [PATH].\n\
450 Please make sure it is installed and is in PATH!\n\
451 Hint: set the [{}] environment variable to provide its path explicitly.",
452 binary_name, env_var
453 );
454 if let Some(hint) = hint {
455 message.push_str(&format!("\n{}", hint.bright_black()));
456 }
457 format!("{}", renderer.render(Level::Error.title(&message)))
458 }
459 Self::ProducedFile { path, wrote } => {
460 let path = relative_to_cwd(path);
461 let title = if wrote {
462 format!("hax: wrote file {}", path.display())
463 } else {
464 format!("hax: unchanged file {}", path.display())
465 };
466 format!("{}", renderer.render(Level::Info.title(&title)))
467 }
468 Self::HaxEngineFailure { exit_code } => {
469 let title = format!(
470 "hax: {} exited with non-zero code {}",
471 ENGINE_BINARY_NAME, exit_code,
472 );
473 format!("{}", renderer.render(Level::Error.title(&title)))
474 }
475 Self::ProfilingData(data) => {
476 fn format_with_dot(shift: u32, n: u64) -> String {
477 let factor = 10u64.pow(shift);
478 format!("{}.{}", n / factor, n % factor)
479 }
480 let title = format!(
481 "hax[profiling]: {}: {}ms, memory={}, {} item{}{}",
482 data.context,
483 format_with_dot(6, data.time_ns),
484 data.memory,
485 data.quantity,
486 if data.quantity > 1 { "s" } else { "" },
487 if data.errored {
488 " (note: this failed!)"
489 } else {
490 ""
491 }
492 );
493 format!("{}", renderer.render(Level::Info.title(&title)))
494 }
495 Self::Stats { errors_per_item } => {
496 let success_items = errors_per_item.iter().filter(|(_, n)| *n == 0).count();
497 let total = errors_per_item.len();
498 let title = format!(
499 "hax: {}/{} items were successfully translated ({}% success rate)",
500 success_items,
501 total,
502 (success_items * 100) / total
503 );
504 format!("{}", renderer.render(Level::Info.title(&title)))
505 }
506 Self::CargoBuildFailure => {
507 let title =
508 "hax: running `cargo build` was not successful, continuing anyway.".to_string();
509 format!("{}", renderer.render(Level::Warning.title(&title)))
510 }
511 Self::WarnExperimentalBackend { backend } => {
512 let title = format!(
513 "hax: Experimental backend \"{}\" is work in progress.",
514 backend
515 );
516 format!("{}", renderer.render(Level::Warning.title(&title)))
517 }
518 Self::GenericError { message } => {
519 let title = format!("hax: {}", message);
520 format!("{}", renderer.render(Level::Error.title(&title)))
521 }
522 Self::GenericWarning { message } => {
523 let title = format!("hax: {}", message);
524 format!("{}", renderer.render(Level::Warning.title(&title)))
525 }
526 Self::Step { verb, target } => {
527 use colored::Colorize;
528 format!("{:>12} {}", verb.bold().green(), target)
529 }
530 Self::SubprocessOutput { prefix, line } => {
531 format!("{:>12} > {}", prefix, line)
532 }
533 Self::OutputTruncated {
534 prefix,
535 remaining,
536 log_path,
537 } => {
538 format!(
539 "{:>12} > ... ({} more lines, full output in {})",
540 prefix,
541 remaining,
542 log_path.display()
543 )
544 }
545 Self::UnsupportedOption { option, backend } => {
546 let title = format!(
547 "hax: option {} is not supported by the {} backend and will be ignored",
548 option, backend
549 );
550 format!("{}", renderer.render(Level::Warning.title(&title)))
551 }
552 Self::HaxTomlWarning { path, message } => {
553 let title = format!("hax: {}: {}", path.display(), message);
554 format!("{}", renderer.render(Level::Warning.title(&title)))
555 }
556 Self::HaxTomlError { path, message } => {
557 let title = format!("hax: {}: {}", path.display(), message);
558 format!("{}", renderer.render(Level::Error.title(&title)))
559 }
560 Self::MemberToolOverrides {
561 crate_name,
562 path,
563 entries,
564 } => {
565 let title = format!(
566 "hax: crate `{}` overrides the workspace tool configuration ({}) in {}. \
567 Prefer a single workspace-wide pin where possible.",
568 crate_name,
569 entries.join(", "),
570 path.display()
571 );
572 format!("{}", renderer.render(Level::Warning.title(&title)))
573 }
574 Self::HaxLibIncompatible {
575 crate_name,
576 found,
577 binary,
578 expected,
579 newer,
580 } => {
581 let remedy = if newer {
582 format!(
583 "update cargo-hax to the release matching hax-lib {found}, or pin\n\
584 the `hax-lib` dependency to {expected} in Cargo.toml"
585 )
586 } else {
587 format!(
588 "update the `hax-lib` dependency to {expected}, e.g. with\n\
589 `cargo update -p hax-lib --precise {expected}`, or install cargo-hax {found}"
590 )
591 };
592 let title = format!(
593 "incompatible `hax-lib` version\n\n\
594 this cargo-hax binary ({binary}) requires hax-lib {expected}\n\
595 found hax-lib {found} in Cargo.lock (crate `{crate_name}`)\n\n\
596 {remedy}"
597 );
598 format!("{}", renderer.render(Level::Error.title(&title)))
599 }
600 Self::NonDefaultToolVersion { tool, used, tested } => {
601 let title =
602 format!("hax: using {tool} {used}; this hax release was tested with {tested}");
603 format!("{}", renderer.render(Level::Info.title(&title)))
604 }
605 Self::LakefilePinDrift {
606 path,
607 name,
608 found,
609 expected,
610 } => {
611 let path = relative_to_cwd(path);
612 let title = format!(
613 "hax: {} pins {name} {found}; the current configuration expects {expected}",
614 path.display()
615 );
616 let remedy = "update the pin, or delete the file and re-run to regenerate it";
617 warn_with_help(&renderer, &title, remedy)
618 }
619 Self::RootModuleMissingImport { path, import } => {
620 let path = relative_to_cwd(path);
621 let title = format!(
622 "hax: {} does not import {import}, so `lake build` will not \
623 check that file",
624 path.display()
625 );
626 let remedy = format!(
627 "add `import {import}`, or comment it out (`-- import {import}`) \
628 to silence this warning"
629 );
630 warn_with_help(&renderer, &title, &remedy)
631 }
632 Self::RootModuleStaleImport { path, import } => {
633 let path = relative_to_cwd(path);
634 let title = format!(
635 "hax: {} imports {import}, but the extraction no longer \
636 produces that file",
637 path.display()
638 );
639 let remedy = "remove or comment out the import line";
640 warn_with_help(&renderer, &title, remedy)
641 }
642 Self::UnverifiedInstall { tool, version, url } => {
643 let title = format!(
644 "{tool} {version} is not in this release's manifest; \
645 installing without checksum verification"
646 );
647 let source = format!("source {url}");
648 let remedy = format!(
649 "once a checksum ships, run \
650 `cargo hax tools install {tool}@{version} --force` to verify"
651 );
652 format!(
653 "{}",
654 renderer.render(
655 Level::Warning
656 .title(&title)
657 .footer(Level::Note.title(&source))
658 .footer(Level::Help.title(&remedy))
659 )
660 )
661 }
662 Self::StrayHaxToml { path } => {
663 let title = format!(
664 "hax: found {} outside the workspace root and member crate roots; \
665 it has no effect and is ignored",
666 path.display()
667 );
668 format!("{}", renderer.render(Level::Warning.title(&title)))
669 }
670 Self::CachedUnverifiedToolInUse { tool, version } => {
671 let title = format!(
672 "using {tool} {version} from the cache; it was installed \
673 without checksum verification"
674 );
675 let remedy = format!(
676 "run `cargo hax tools install {tool}@{version} --force` to \
677 re-download and verify it once a checksum ships"
678 );
679 warn_with_help(&renderer, &title, &remedy)
680 }
681 Self::ToolsShow {
682 tools,
683 versions,
684 hax_lib,
685 member_overrides,
686 } => render_tools_show(&tools, &versions, &hax_lib, &member_overrides),
687 Self::ToolsList {
688 tools,
689 installed_only,
690 } => render_tools_list(&tools, installed_only),
691 Self::ToolsInstalled { installed } => render_tools_installed(&installed),
692 Self::ToolRemoved { tool, version } => {
693 use colored::Colorize;
694 format!("{:>12} {tool} {version}", "Removed".bold().green())
695 }
696 Self::ToolsCleaned { removed } => {
697 use colored::Colorize;
698 let noun = if removed == 1 {
699 "tool version"
700 } else {
701 "tool versions"
702 };
703 format!("{:>12} {removed} {noun}", "Removed".bold().green())
704 }
705 Self::ToolsPinned {
706 path,
707 changes,
708 skipped,
709 } => render_tools_pinned(&path, &changes, &skipped),
710 Self::ScenarioDryRun {
711 name,
712 package,
713 lines,
714 } => {
715 let mut block = vec![format!("scenario `{name}` (package `{package}`):")];
716 block.extend(lines.iter().map(|line| format!(" {line}")));
717 block.join("\n")
718 }
719 Self::ScenarioSummary { total, failed } => {
720 let plural = |n: usize| if n == 1 { "" } else { "s" };
721 if failed.is_empty() {
722 let title = format!("hax: {total} scenario{} extracted", plural(total));
723 format!("{}", renderer.render(Level::Info.title(&title)))
724 } else {
725 let title = format!(
726 "hax: {} of {total} scenario{} failed: {}",
727 failed.len(),
728 plural(total),
729 failed.join(", ")
730 );
731 format!("{}", renderer.render(Level::Error.title(&title)))
732 }
733 }
734 }
735 }
736}
737
738const HAX_LIB_ROW: &str = "hax-lib";
740
741fn resolution_rows(
743 entries: &[ToolResolution],
744 name_width: usize,
745 value_width: usize,
746) -> impl Iterator<Item = String> + '_ {
747 entries.iter().map(move |entry| {
748 format!(
749 " {name:name_width$} {value:value_width$} ({source})",
750 name = entry.name,
751 value = entry.value(),
752 source = entry.source,
753 )
754 })
755}
756
757fn render_tools_show(
761 tools: &[ToolResolution],
762 versions: &[ToolResolution],
763 hax_lib: &[HaxLibStatus],
764 member_overrides: &[MemberOverride],
765) -> String {
766 let all = || {
767 tools.iter().chain(versions).chain(
768 member_overrides
769 .iter()
770 .flat_map(|member| member.tools.iter().chain(&member.versions)),
771 )
772 };
773 let name_width = all()
776 .map(|entry| entry.name.len())
777 .chain(hax_lib.iter().map(|_| HAX_LIB_ROW.len()))
778 .max()
779 .unwrap_or(0);
780 let value_width = all()
781 .map(|entry| entry.value().len())
782 .chain(hax_lib.iter().map(|status| status.version.len()))
783 .max()
784 .unwrap_or(0);
785 let hax_lib_row = |status: &HaxLibStatus, annotation: String| {
786 format!(
787 " {name:name_width$} {value:value_width$} ({annotation})",
788 name = HAX_LIB_ROW,
789 value = status.version,
790 )
791 };
792
793 let mut lines = vec!["tools:".to_string()];
794 lines.extend(resolution_rows(tools, name_width, value_width));
795 lines.push(String::new());
796 lines.push("versions:".to_string());
797 lines.extend(resolution_rows(versions, name_width, value_width));
798
799 let uniform = hax_lib.iter().all(|status| {
802 (&status.version, status.compatibility) == (&hax_lib[0].version, hax_lib[0].compatibility)
803 });
804 match hax_lib {
805 [] => {}
806 [first, ..] => {
807 lines.push(String::new());
808 lines.push("libraries:".to_string());
809 if uniform {
810 lines.push(hax_lib_row(
811 first,
812 first.compatibility.describe().to_string(),
813 ));
814 } else {
815 lines.extend(hax_lib.iter().map(|status| {
816 hax_lib_row(
817 status,
818 format!(
819 "crate `{}`: {}",
820 status.crate_name,
821 status.compatibility.describe()
822 ),
823 )
824 }));
825 }
826 }
827 }
828
829 for member in member_overrides {
830 lines.push(String::new());
831 lines.push(format!("crate `{}` (overrides):", member.crate_name));
832 lines.extend(resolution_rows(&member.tools, name_width, value_width));
833 lines.extend(resolution_rows(&member.versions, name_width, value_width));
834 }
835 lines.join("\n")
836}
837
838fn render_tools_list(tools: &[ToolListing], installed_only: bool) -> String {
841 let mut blocks = Vec::new();
842 for listing in tools {
843 let mut lines = vec![format!("{}:", listing.tool)];
844 if listing.versions.is_empty() {
845 lines.push(format!(
846 " ({})",
847 if installed_only {
848 "none installed"
849 } else {
850 "none"
851 }
852 ));
853 }
854 let width = listing
856 .versions
857 .iter()
858 .map(|version| version.version.len())
859 .max()
860 .unwrap_or(0);
861 for version in &listing.versions {
862 let mut marks = Vec::new();
863 if version.default {
864 marks.push("default".to_string());
865 }
866 if version.installed {
867 marks.push("installed".to_string());
868 if !version.verified {
869 marks.push("unverified".to_string());
870 }
871 }
872 if !version.in_manifest {
873 marks.push("not in manifest".to_string());
874 }
875 lines.push(if marks.is_empty() {
876 format!(" {}", version.version)
877 } else {
878 format!(
879 " {version:width$} ({marks})",
880 version = version.version,
881 marks = marks.join(", ")
882 )
883 });
884 }
885 if listing.omitted > 0 {
886 lines.push(format!(
887 " ... {} older versions omitted (use --all)",
888 listing.omitted
889 ));
890 }
891 blocks.push(lines.join("\n"));
892 }
893 blocks.join("\n\n")
894}
895
896fn render_tools_pinned(
899 path: &std::path::Path,
900 changes: &[PinChange],
901 skipped: &[String],
902) -> String {
903 use colored::Colorize;
904 let path = relative_to_cwd(path.to_path_buf());
905 let mut lines: Vec<String> = skipped
906 .iter()
907 .map(|name| {
908 format!(
909 "{:>12} {name} (pinned to a path)",
910 "Skipped".bold().yellow()
911 )
912 })
913 .chain(changes.iter().map(|change| {
914 let previous = match &change.previous {
915 Some(previous) => format!(" (was {previous})"),
916 None => String::new(),
917 };
918 format!(
919 "{:>12} {} {}{previous}",
920 "Pinned".bold().green(),
921 change.name,
922 change.version
923 )
924 }))
925 .collect();
926 lines.push(if !changes.is_empty() {
927 format!(
928 "{:>12} {} (run `cargo hax tools install` to pre-fetch)",
929 "Wrote".bold().green(),
930 path.display()
931 )
932 } else if !skipped.is_empty() {
933 format!("{:>12} {}", "Unchanged".bold().green(), path.display())
934 } else {
935 format!(
936 "{:>12} {} already pins these versions",
937 "Unchanged".bold().green(),
938 path.display()
939 )
940 });
941 lines.join("\n")
942}
943
944fn render_tools_installed(installed: &[InstalledTool]) -> String {
946 use colored::Colorize;
947 installed
948 .iter()
949 .map(|entry| {
950 let (verb, verified) = match entry.status {
951 InstallStatus::Cached { verified } => ("Cached", verified),
952 InstallStatus::Installed { verified } => ("Installed", verified),
953 };
954 let suffix = if verified { "" } else { " (unverified)" };
955 format!(
956 "{:>12} {} {}{}",
957 verb.bold().green(),
958 entry.tool,
959 entry.version,
960 suffix
961 )
962 })
963 .collect::<Vec<_>>()
964 .join("\n")
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970
971 #[test]
972 fn a_reported_error_must_not_exit_successfully() {
973 let warning = HaxMessage::GenericWarning {
974 message: "w".into(),
975 };
976 assert!(!warning.is_error());
977
978 let error = HaxMessage::GenericError {
979 message: "e".into(),
980 };
981 assert!(error.is_error());
982 error.report(MessageFormat::Json, None);
983 assert!(errors_reported());
984 }
985}