1#![forbid(unsafe_code)]
7
8use petri_artifacts_common::tags::MachineArch;
9use petri_artifacts_core::ErasedArtifactHandle;
10use std::env::consts::EXE_EXTENSION;
11use std::path::Path;
12use std::path::PathBuf;
13use vmm_test_images::KnownTestArtifacts;
14
15pub struct OpenvmmKnownPathsTestArtifactResolver<'a>(&'a str);
19
20impl<'a> OpenvmmKnownPathsTestArtifactResolver<'a> {
21 pub fn new(test_name: &'a str) -> Self {
23 Self(test_name)
24 }
25}
26
27impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifactResolver<'_> {
28 #[rustfmt::skip]
29 fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
30 use petri_artifacts_common::artifacts as common;
31 use petri_artifacts_vmm_test::artifacts::*;
32
33 match id {
34 _ if id == common::PIPETTE_WINDOWS_X64 => pipette_path(MachineArch::X86_64, PipetteFlavor::Windows),
35 _ if id == common::PIPETTE_LINUX_X64 => pipette_path(MachineArch::X86_64, PipetteFlavor::Linux),
36 _ if id == common::PIPETTE_WINDOWS_AARCH64 => pipette_path(MachineArch::Aarch64, PipetteFlavor::Windows),
37 _ if id == common::PIPETTE_LINUX_AARCH64 => pipette_path(MachineArch::Aarch64, PipetteFlavor::Linux),
38
39 _ if id == common::TEST_LOG_DIRECTORY => test_log_directory_path(self.0),
40
41 _ if id == OPENVMM_NATIVE => openvmm_native_executable_path(),
42
43 _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_X64 => linux_direct_x64_test_kernel_path(),
44 _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64 => linux_direct_arm_image_path(),
45 _ if id == loadable::LINUX_DIRECT_TEST_INITRD_X64 => linux_direct_test_initrd_path(MachineArch::X86_64),
46 _ if id == loadable::LINUX_DIRECT_TEST_INITRD_AARCH64 => linux_direct_test_initrd_path(MachineArch::Aarch64),
47
48 _ if id == loadable::PCAT_FIRMWARE_X64 => pcat_firmware_path(),
49 _ if id == loadable::SVGA_FIRMWARE_X64 => svga_firmware_path(),
50 _ if id == loadable::UEFI_FIRMWARE_X64 => uefi_firmware_path(MachineArch::X86_64),
51 _ if id == loadable::UEFI_FIRMWARE_AARCH64 => uefi_firmware_path(MachineArch::Aarch64),
52
53 _ if id == openhcl_igvm::LATEST_STANDARD_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Standard),
54 _ if id == openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel),
55 _ if id == openhcl_igvm::LATEST_CVM_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Cvm),
56 _ if id == openhcl_igvm::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::LinuxDirect),
57 _ if id == openhcl_igvm::LATEST_STANDARD_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::Standard),
58 _ if id == openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel),
59
60 _ if id == openhcl_igvm::RELEASE_25_05_STANDARD_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Release2505, OpenhclFlavor::Standard),
61 _ if id == openhcl_igvm::RELEASE_25_05_LINUX_DIRECT_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Release2505, OpenhclFlavor::LinuxDirect),
62 _ if id == openhcl_igvm::RELEASE_25_05_STANDARD_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Release2505, OpenhclFlavor::Standard),
63
64 _ if id == openhcl_igvm::um_bin::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_extras_path(OpenhclVersion::Latest,OpenhclFlavor::LinuxDirect,OpenhclExtras::UmBin),
65 _ if id == openhcl_igvm::um_dbg::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_extras_path(OpenhclVersion::Latest,OpenhclFlavor::LinuxDirect,OpenhclExtras::UmDbg),
66
67 _ if id == test_vhd::GUEST_TEST_UEFI_X64 => guest_test_uefi_disk_path(MachineArch::X86_64),
68 _ if id == test_vhd::GUEST_TEST_UEFI_AARCH64 => guest_test_uefi_disk_path(MachineArch::Aarch64),
69 _ if id == test_vhd::GEN1_WINDOWS_DATA_CENTER_CORE2022_X64 => get_test_artifact_path(KnownTestArtifacts::Gen1WindowsDataCenterCore2022X64Vhd),
70 _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2022_X64 => get_test_artifact_path(KnownTestArtifacts::Gen2WindowsDataCenterCore2022X64Vhd),
71 _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64 => get_test_artifact_path(KnownTestArtifacts::Gen2WindowsDataCenterCore2025X64Vhd),
72 _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64_PREPPED => get_prepped_test_artifact_path(KnownTestArtifacts::Gen2WindowsDataCenterCore2025X64Vhd),
73 _ if id == test_vhd::FREE_BSD_13_2_X64 => get_test_artifact_path(KnownTestArtifacts::FreeBsd13_2X64Vhd),
74 _ if id == test_vhd::UBUNTU_2404_SERVER_X64 => get_test_artifact_path(KnownTestArtifacts::Ubuntu2404ServerX64Vhd),
75 _ if id == test_vhd::UBUNTU_2504_SERVER_X64 => get_test_artifact_path(KnownTestArtifacts::Ubuntu2504ServerX64Vhd),
76 _ if id == test_vhd::UBUNTU_2404_SERVER_AARCH64 => get_test_artifact_path(KnownTestArtifacts::Ubuntu2404ServerAarch64Vhd),
77 _ if id == test_vhd::WINDOWS_11_ENTERPRISE_AARCH64 => get_test_artifact_path(KnownTestArtifacts::Windows11EnterpriseAarch64Vhdx),
78
79 _ if id == test_iso::FREE_BSD_13_2_X64 => get_test_artifact_path(KnownTestArtifacts::FreeBsd13_2X64Iso),
80
81 _ if id == test_vmgs::VMGS_WITH_BOOT_ENTRY => get_test_artifact_path(KnownTestArtifacts::VmgsWithBootEntry),
82
83 _ if id == tmks::TMK_VMM_NATIVE => tmk_vmm_native_executable_path(),
84 _ if id == tmks::TMK_VMM_LINUX_X64_MUSL => tmk_vmm_paravisor_path(MachineArch::X86_64),
85 _ if id == tmks::TMK_VMM_LINUX_AARCH64_MUSL => tmk_vmm_paravisor_path(MachineArch::Aarch64),
86 _ if id == tmks::SIMPLE_TMK_X64 => simple_tmk_path(MachineArch::X86_64),
87 _ if id == tmks::SIMPLE_TMK_AARCH64 => simple_tmk_path(MachineArch::Aarch64),
88
89 _ if id == VMGSTOOL_NATIVE => vmgstool_native_executable_path(),
90
91 _ => anyhow::bail!("no support for given artifact type"),
92 }
93 }
94}
95
96#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
97enum PipetteFlavor {
98 Windows,
99 Linux,
100}
101
102#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
103enum OpenhclVersion {
104 Latest,
105 Release2505,
106}
107
108#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
109enum OpenhclFlavor {
110 Standard,
111 StandardDevKernel,
112 Cvm,
113 LinuxDirect,
114}
115
116#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
117enum OpenhclExtras {
118 UmBin,
119 UmDbg,
120}
121
122fn target_arch_path(arch: MachineArch) -> &'static str {
124 match arch {
125 MachineArch::X86_64 => "x86_64",
126 MachineArch::Aarch64 => "aarch64",
127 }
128}
129
130fn get_test_artifact_path(artifact: KnownTestArtifacts) -> Result<PathBuf, anyhow::Error> {
131 let images_dir = std::env::var("VMM_TEST_IMAGES");
132 let full_path = Path::new(images_dir.as_deref().unwrap_or("images"));
133
134 get_path(
135 full_path,
136 artifact.filename(),
137 MissingCommand::Xtask {
138 xtask_args: &[
139 "guest-test",
140 "download-image",
141 "--artifacts",
142 &artifact.name(),
143 ],
144 description: "test artifact",
145 },
146 )
147}
148
149fn get_prepped_test_artifact_path(artifact: KnownTestArtifacts) -> Result<PathBuf, anyhow::Error> {
150 let images_dir = std::env::var("VMM_TEST_IMAGES");
151 let full_path = Path::new(images_dir.as_deref().unwrap_or("images"));
152 let prepped_filename = artifact.filename().replace(".vhd", "-prepped.vhd");
153
154 get_path(
155 full_path,
156 prepped_filename,
157 MissingCommand::Run {
158 description: "prepped test image",
159 package: "prep_steps",
160 },
161 )
162}
163
164fn guest_test_uefi_disk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
166 get_path(
168 format!("target/{}-unknown-uefi/debug", target_arch_path(arch)),
169 "guest_test_uefi.img",
170 MissingCommand::Xtask {
171 xtask_args: &[
172 "guest-test",
173 "uefi",
174 &format!(
175 "--boot{}",
176 match arch {
177 MachineArch::X86_64 => "x64",
178 MachineArch::Aarch64 => "aa64",
179 }
180 ),
181 ],
182 description: "guest_test_uefi image",
183 },
184 )
185}
186
187fn pipette_path(arch: MachineArch, os_flavor: PipetteFlavor) -> anyhow::Result<PathBuf> {
189 let (target_suffixes, binary) = match os_flavor {
192 PipetteFlavor::Windows => (vec!["pc-windows-msvc", "pc-windows-gnu"], "pipette.exe"),
193 PipetteFlavor::Linux => (vec!["unknown-linux-musl"], "pipette"),
194 };
195 for (index, target_suffix) in target_suffixes.iter().enumerate() {
196 let target = format!("{}-{}", target_arch_path(arch), target_suffix);
197 match get_path(
198 format!("target/{target}/debug"),
199 binary,
200 MissingCommand::Build {
201 package: "pipette",
202 target: Some(&target),
203 },
204 ) {
205 Ok(path) => return Ok(path),
206 Err(err) => {
207 if index < target_suffixes.len() - 1 {
208 continue;
209 } else {
210 anyhow::bail!(
211 "None of the suffixes {:?} had `pipette` built, {err:?}",
212 target_suffixes
213 );
214 }
215 }
216 }
217 }
218
219 unreachable!()
220}
221
222fn openvmm_native_executable_path() -> anyhow::Result<PathBuf> {
224 get_output_executable_path("openvmm")
225}
226
227fn tmk_vmm_native_executable_path() -> anyhow::Result<PathBuf> {
229 get_output_executable_path("tmk_vmm")
230}
231
232fn vmgstool_native_executable_path() -> anyhow::Result<PathBuf> {
234 get_output_executable_path("vmgstool")
235}
236
237fn tmk_vmm_paravisor_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
238 let target = match arch {
239 MachineArch::X86_64 => "x86_64-unknown-linux-musl",
240 MachineArch::Aarch64 => "aarch64-unknown-linux-musl",
241 };
242 get_path(
243 format!("target/{target}/debug"),
244 "tmk_vmm",
245 MissingCommand::Build {
246 package: "tmk_vmm",
247 target: Some(target),
248 },
249 )
250}
251
252fn simple_tmk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
254 let arch_str = match arch {
255 MachineArch::X86_64 => "x86_64",
256 MachineArch::Aarch64 => "aarch64",
257 };
258 let target = match arch {
259 MachineArch::X86_64 => "x86_64-unknown-none",
260 MachineArch::Aarch64 => "aarch64-minimal_rt-none",
261 };
262 get_path(
263 format!("target/{target}/debug"),
264 "simple_tmk",
265 MissingCommand::Custom {
266 description: "simple_tmk",
267 cmd: &format!(
268 "RUSTC_BOOTSTRAP=1 cargo build -p simple_tmk --config openhcl/minimal_rt/{arch_str}-config.toml"
269 ),
270 },
271 )
272}
273
274fn linux_direct_x64_test_kernel_path() -> anyhow::Result<PathBuf> {
276 get_path(
277 ".packages/underhill-deps-private",
278 "x64/vmlinux",
279 MissingCommand::Restore {
280 description: "linux direct test kernel",
281 },
282 )
283}
284
285fn linux_direct_test_initrd_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
287 get_path(
288 ".packages/underhill-deps-private",
289 format!(
290 "{}/initrd",
291 match arch {
292 MachineArch::X86_64 => "x64",
293 MachineArch::Aarch64 => "aarch64",
294 }
295 ),
296 MissingCommand::Restore {
297 description: "linux direct test initrd",
298 },
299 )
300}
301
302fn linux_direct_arm_image_path() -> anyhow::Result<PathBuf> {
304 get_path(
305 ".packages/underhill-deps-private",
306 "aarch64/Image",
307 MissingCommand::Restore {
308 description: "linux direct test kernel",
309 },
310 )
311}
312
313fn pcat_firmware_path() -> anyhow::Result<PathBuf> {
315 get_path(
316 ".packages",
317 "Microsoft.Windows.VmFirmware.Pcat.amd64fre/content/vmfirmwarepcat.dll",
318 MissingCommand::Restore {
319 description: "PCAT firmware binary",
320 },
321 )
322}
323
324fn svga_firmware_path() -> anyhow::Result<PathBuf> {
326 get_path(
327 ".packages",
328 "Microsoft.Windows.VmEmulatedDevices.amd64fre/content/VmEmulatedDevices.dll",
329 MissingCommand::Restore {
330 description: "SVGA firmware binary",
331 },
332 )
333}
334
335fn uefi_firmware_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
337 get_path(
338 ".packages",
339 match arch {
340 MachineArch::X86_64 => {
341 "hyperv.uefi.mscoreuefi.x64.RELEASE/MsvmX64/RELEASE_VS2022/FV/MSVM.fd"
342 }
343 MachineArch::Aarch64 => {
344 "hyperv.uefi.mscoreuefi.AARCH64.RELEASE/MsvmAARCH64/RELEASE_VS2022/FV/MSVM.fd"
345 }
346 },
347 MissingCommand::Restore {
348 description: "UEFI firmware binary",
349 },
350 )
351}
352
353fn openhcl_bin_path(
355 arch: MachineArch,
356 version: OpenhclVersion,
357 flavor: OpenhclFlavor,
358) -> anyhow::Result<PathBuf> {
359 let (path, name, cmd) = match (arch, version, flavor) {
360 (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Standard) => (
361 "flowey-out/artifacts/build-igvm/debug/x64",
362 "openhcl-x64.bin",
363 MissingCommand::XFlowey {
364 description: "OpenHCL IGVM file",
365 xflowey_args: &["build-igvm", "x64"],
366 },
367 ),
368 (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel) => (
369 "flowey-out/artifacts/build-igvm/debug/x64-devkern",
370 "openhcl-x64-devkern.bin",
371 MissingCommand::XFlowey {
372 description: "OpenHCL IGVM file",
373 xflowey_args: &["build-igvm", "x64-devkern"],
374 },
375 ),
376 (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Cvm) => (
377 "flowey-out/artifacts/build-igvm/debug/x64-cvm",
378 "openhcl-x64-cvm.bin",
379 MissingCommand::XFlowey {
380 description: "OpenHCL IGVM file",
381 xflowey_args: &["build-igvm", "x64-cvm"],
382 },
383 ),
384 (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::LinuxDirect) => (
385 "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
386 "openhcl-x64-test-linux-direct.bin",
387 MissingCommand::XFlowey {
388 description: "OpenHCL IGVM file",
389 xflowey_args: &["build-igvm", "x64-test-linux-direct"],
390 },
391 ),
392 (MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::Standard) => (
393 "flowey-out/artifacts/build-igvm/debug/aarch64",
394 "openhcl-aarch64.bin",
395 MissingCommand::XFlowey {
396 description: "OpenHCL IGVM file",
397 xflowey_args: &["build-igvm", "aarch64"],
398 },
399 ),
400 (MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel) => (
401 "flowey-out/artifacts/build-igvm/debug/aarch64-devkern",
402 "openhcl-aarch64-devkern.bin",
403 MissingCommand::XFlowey {
404 description: "OpenHCL IGVM file",
405 xflowey_args: &["build-igvm", "aarch64-devkern"],
406 },
407 ),
408 (MachineArch::X86_64, OpenhclVersion::Release2505, OpenhclFlavor::LinuxDirect) => (
409 "flowey-out/artifacts/latest-release-igvm-files",
410 "release-2505-x64-direct-openhcl.bin",
411 MissingCommand::XFlowey {
412 description: "Previous OpenHCL release IGVM file",
413 xflowey_args: &["restore-packages"],
414 },
415 ),
416 (MachineArch::X86_64, OpenhclVersion::Release2505, OpenhclFlavor::Standard) => (
417 "flowey-out/artifacts/latest-release-igvm-files",
418 "release-2505-x64-openhcl.bin",
419 MissingCommand::XFlowey {
420 description: "Previous OpenHCL release IGVM file",
421 xflowey_args: &["restore-packages"],
422 },
423 ),
424 (MachineArch::Aarch64, OpenhclVersion::Release2505, OpenhclFlavor::Standard) => (
425 "flowey-out/artifacts/latest-release-igvm-files",
426 "release-2505-aarch64-openhcl.bin",
427 MissingCommand::XFlowey {
428 description: "Previous OpenHCL release IGVM file",
429 xflowey_args: &["restore-packages"],
430 },
431 ),
432 _ => anyhow::bail!("no openhcl bin with given arch, version, and flavor"),
433 };
434
435 get_path(path, name, cmd)
436}
437
438fn openhcl_extras_path(
440 version: OpenhclVersion,
441 flavor: OpenhclFlavor,
442 item: OpenhclExtras,
443) -> anyhow::Result<PathBuf> {
444 if !matches!(version, OpenhclVersion::Latest) || !matches!(flavor, OpenhclFlavor::LinuxDirect) {
445 anyhow::bail!("Debug symbol path currently only available for LATEST_LINUX_DIRECT_TEST")
446 }
447
448 let (path, name) = match item {
449 OpenhclExtras::UmBin => (
450 "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
451 "openvmm_hcl_msft",
452 ),
453 OpenhclExtras::UmDbg => (
454 "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
455 "openvmm_hcl_msft.dbg",
456 ),
457 };
458
459 get_path(
460 path,
461 name,
462 MissingCommand::XFlowey {
463 description: "OpenHCL IGVM file",
464 xflowey_args: &["build-igvm", "x64-test-linux-direct"],
465 },
466 )
467}
468
469fn test_log_directory_path(test_name: &str) -> anyhow::Result<PathBuf> {
471 let root = if let Some(path) = std::env::var_os("TEST_OUTPUT_PATH") {
472 PathBuf::from(path)
473 } else {
474 get_repo_root()?.join("vmm_test_results")
475 };
476 let path = root.join(test_name.replace("::", "__"));
479 fs_err::create_dir_all(&path)?;
480 Ok(path)
481}
482
483const VMM_TESTS_DIR_ENV_VAR: &str = "VMM_TESTS_CONTENT_DIR";
484
485pub fn get_repo_root() -> anyhow::Result<PathBuf> {
487 Ok(Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
488}
489
490pub fn get_path(
504 search_path: impl AsRef<Path>,
505 file_name: impl AsRef<Path>,
506 missing_cmd: MissingCommand<'_>,
507) -> anyhow::Result<PathBuf> {
508 let search_path = search_path.as_ref();
509 let file_name = file_name.as_ref();
510 if file_name.is_absolute() {
511 anyhow::bail!("{} should be a relative path", file_name.display());
512 }
513
514 if let Ok(env_dir) = std::env::var(VMM_TESTS_DIR_ENV_VAR) {
515 let full_path = Path::new(&env_dir).join(file_name);
516 if full_path.try_exists()? {
517 return Ok(full_path);
518 }
519 }
520
521 let file_path = if search_path.is_absolute() {
522 search_path.to_owned()
523 } else {
524 get_repo_root()?.join(search_path)
525 };
526
527 let full_path = file_path.join(file_name);
528 if !full_path.exists() {
529 eprintln!("Failed to find {:?}.", full_path);
530 missing_cmd.to_error()?;
531 }
532
533 Ok(full_path)
534}
535
536pub fn get_output_executable_path(name: &str) -> anyhow::Result<PathBuf> {
540 let mut path: PathBuf = std::env::current_exe()?;
541 if path.parent().and_then(|x| x.file_name()).unwrap() == "deps" {
544 path.pop();
545 }
546
547 get_path(
548 path.parent().unwrap(),
549 Path::new(name).with_extension(EXE_EXTENSION),
550 MissingCommand::Build {
551 package: name,
552 target: None,
553 },
554 )
555}
556
557#[derive(Copy, Clone)]
560#[expect(missing_docs)] pub enum MissingCommand<'a> {
562 Build {
564 package: &'a str,
565 target: Option<&'a str>,
566 },
567 Run {
569 description: &'a str,
570 package: &'a str,
571 },
572 Xtask {
574 description: &'a str,
575 xtask_args: &'a [&'a str],
576 },
577 XFlowey {
579 description: &'a str,
580 xflowey_args: &'a [&'a str],
581 },
582 Restore { description: &'a str },
584 Custom { description: &'a str, cmd: &'a str },
586}
587
588impl MissingCommand<'_> {
589 fn to_error(self) -> anyhow::Result<()> {
590 match self {
591 MissingCommand::Build { package, target } => anyhow::bail!(
592 "Failed to find {package} binary. Run `cargo build {target_args}-p {package}` to build it.",
593 target_args =
594 target.map_or(String::new(), |target| format!("--target {} ", target)),
595 ),
596 MissingCommand::Run {
597 description,
598 package,
599 } => anyhow::bail!(
600 "Failed to find {}. Run `cargo run -p {}` to create it.",
601 description,
602 package
603 ),
604 MissingCommand::Xtask {
605 description,
606 xtask_args: args,
607 } => {
608 anyhow::bail!(
609 "Failed to find {}. Run `cargo xtask {}` to create it.",
610 description,
611 args.join(" ")
612 )
613 }
614 MissingCommand::XFlowey {
615 description,
616 xflowey_args: args,
617 } => anyhow::bail!(
618 "Failed to find {}. Run `cargo xflowey {}` to create it.",
619 description,
620 args.join(" ")
621 ),
622 MissingCommand::Restore { description } => {
623 anyhow::bail!(
624 "Failed to find {}. Run `cargo xflowey restore-packages`.",
625 description
626 )
627 }
628 MissingCommand::Custom { description, cmd } => {
629 anyhow::bail!(
630 "Failed to find {}. Run `{}` to create it.",
631 description,
632 cmd
633 )
634 }
635 }
636 }
637}