petri_artifact_resolver_openvmm_known_paths/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! See [`OpenvmmKnownPathsTestArtifactResolver`].
5
6#![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
15/// An implementation of [`petri_artifacts_core::ResolveTestArtifact`]
16/// that resolves artifacts to various "known paths" within the context of
17/// the OpenVMM repository.
18pub struct OpenvmmKnownPathsTestArtifactResolver<'a>(&'a str);
19
20impl<'a> OpenvmmKnownPathsTestArtifactResolver<'a> {
21    /// Creates a new resolver for a test with the given name.
22    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::um_bin::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_extras_path(OpenhclVersion::Latest,OpenhclFlavor::LinuxDirect,OpenhclExtras::UmBin),
61            _ if id == openhcl_igvm::um_dbg::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_extras_path(OpenhclVersion::Latest,OpenhclFlavor::LinuxDirect,OpenhclExtras::UmDbg),
62
63            _ if id == test_vhd::GUEST_TEST_UEFI_X64 => guest_test_uefi_disk_path(MachineArch::X86_64),
64            _ if id == test_vhd::GUEST_TEST_UEFI_AARCH64 => guest_test_uefi_disk_path(MachineArch::Aarch64),
65            _ if id == test_vhd::GEN1_WINDOWS_DATA_CENTER_CORE2022_X64 => get_test_artifact_path(KnownTestArtifacts::Gen1WindowsDataCenterCore2022X64Vhd),
66            _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2022_X64 => get_test_artifact_path(KnownTestArtifacts::Gen2WindowsDataCenterCore2022X64Vhd),
67            _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64 => get_test_artifact_path(KnownTestArtifacts::Gen2WindowsDataCenterCore2025X64Vhd),
68            _ if id == test_vhd::FREE_BSD_13_2_X64 => get_test_artifact_path(KnownTestArtifacts::FreeBsd13_2X64Vhd),
69            _ if id == test_vhd::UBUNTU_2204_SERVER_X64 => get_test_artifact_path(KnownTestArtifacts::Ubuntu2204ServerX64Vhd),
70            _ if id == test_vhd::UBUNTU_2404_SERVER_AARCH64 => get_test_artifact_path(KnownTestArtifacts::Ubuntu2404ServerAarch64Vhd),
71            _ if id == test_vhd::WINDOWS_11_ENTERPRISE_AARCH64 => get_test_artifact_path(KnownTestArtifacts::Windows11EnterpriseAarch64Vhdx),
72
73            _ if id == test_iso::FREE_BSD_13_2_X64 => get_test_artifact_path(KnownTestArtifacts::FreeBsd13_2X64Iso),
74
75            _ if id == test_vmgs::VMGS_WITH_BOOT_ENTRY => get_test_artifact_path(KnownTestArtifacts::VmgsWithBootEntry),
76
77            _ if id == tmks::TMK_VMM_NATIVE => tmk_vmm_native_executable_path(),
78            _ if id == tmks::TMK_VMM_LINUX_X64_MUSL => tmk_vmm_paravisor_path(MachineArch::X86_64),
79            _ if id == tmks::TMK_VMM_LINUX_AARCH64_MUSL => tmk_vmm_paravisor_path(MachineArch::Aarch64),
80            _ if id == tmks::SIMPLE_TMK_X64 => simple_tmk_path(MachineArch::X86_64),
81            _ if id == tmks::SIMPLE_TMK_AARCH64 => simple_tmk_path(MachineArch::Aarch64),
82
83            _ => anyhow::bail!("no support for given artifact type"),
84        }
85    }
86}
87
88#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
89enum PipetteFlavor {
90    Windows,
91    Linux,
92}
93
94#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
95enum OpenhclVersion {
96    Latest,
97}
98
99#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
100enum OpenhclFlavor {
101    Standard,
102    StandardDevKernel,
103    Cvm,
104    LinuxDirect,
105}
106
107#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
108enum OpenhclExtras {
109    UmBin,
110    UmDbg,
111}
112
113/// The architecture specific fragment of the name of the directory used by rust when referring to specific targets.
114fn target_arch_path(arch: MachineArch) -> &'static str {
115    match arch {
116        MachineArch::X86_64 => "x86_64",
117        MachineArch::Aarch64 => "aarch64",
118    }
119}
120
121fn get_test_artifact_path(artifact: KnownTestArtifacts) -> Result<PathBuf, anyhow::Error> {
122    let images_dir = std::env::var("VMM_TEST_IMAGES");
123    let full_path = Path::new(images_dir.as_deref().unwrap_or("images"));
124
125    get_path(
126        full_path,
127        artifact.filename(),
128        MissingCommand::Xtask {
129            xtask_args: &[
130                "guest-test",
131                "download-image",
132                "--artifacts",
133                &artifact.name(),
134            ],
135            description: "test artifact",
136        },
137    )
138}
139
140/// Path to the output location of our guest-test image for UEFI.
141fn guest_test_uefi_disk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
142    // `guest_test_uefi` is always at `{arch}-unknown-uefi/debug`
143    get_path(
144        format!("target/{}-unknown-uefi/debug", target_arch_path(arch)),
145        "guest_test_uefi.img",
146        MissingCommand::Xtask {
147            xtask_args: &[
148                "guest-test",
149                "uefi",
150                &format!(
151                    "--boot{}",
152                    match arch {
153                        MachineArch::X86_64 => "x64",
154                        MachineArch::Aarch64 => "aa64",
155                    }
156                ),
157            ],
158            description: "guest_test_uefi image",
159        },
160    )
161}
162
163/// Path to the output location of the pipette executable.
164fn pipette_path(arch: MachineArch, os_flavor: PipetteFlavor) -> anyhow::Result<PathBuf> {
165    // Always use (statically-built) musl on Linux to avoid needing libc
166    // compatibility.
167    let (target_suffixes, binary) = match os_flavor {
168        PipetteFlavor::Windows => (vec!["pc-windows-msvc", "pc-windows-gnu"], "pipette.exe"),
169        PipetteFlavor::Linux => (vec!["unknown-linux-musl"], "pipette"),
170    };
171    for (index, target_suffix) in target_suffixes.iter().enumerate() {
172        let target = format!("{}-{}", target_arch_path(arch), target_suffix);
173        match get_path(
174            format!("target/{target}/debug"),
175            binary,
176            MissingCommand::Build {
177                package: "pipette",
178                target: Some(&target),
179            },
180        ) {
181            Ok(path) => return Ok(path),
182            Err(err) => {
183                if index < target_suffixes.len() - 1 {
184                    continue;
185                } else {
186                    anyhow::bail!(
187                        "None of the suffixes {:?} had `pipette` built, {err:?}",
188                        target_suffixes
189                    );
190                }
191            }
192        }
193    }
194
195    unreachable!()
196}
197
198/// Path to the output location of the hvlite executable.
199fn openvmm_native_executable_path() -> anyhow::Result<PathBuf> {
200    get_output_executable_path("openvmm")
201}
202
203/// Path to the output location of the tmk_vmm executable.
204fn tmk_vmm_native_executable_path() -> anyhow::Result<PathBuf> {
205    get_output_executable_path("tmk_vmm")
206}
207
208fn tmk_vmm_paravisor_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
209    let target = match arch {
210        MachineArch::X86_64 => "x86_64-unknown-linux-musl",
211        MachineArch::Aarch64 => "aarch64-unknown-linux-musl",
212    };
213    get_path(
214        format!("target/{target}/debug"),
215        "tmk_vmm",
216        MissingCommand::Build {
217            package: "tmk_vmm",
218            target: Some(target),
219        },
220    )
221}
222
223/// Path to the output location of the simple_tmk executable.
224fn simple_tmk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
225    let arch_str = match arch {
226        MachineArch::X86_64 => "x86_64",
227        MachineArch::Aarch64 => "aarch64",
228    };
229    let target = match arch {
230        MachineArch::X86_64 => "x86_64-unknown-none",
231        MachineArch::Aarch64 => "aarch64-minimal_rt-none",
232    };
233    get_path(
234        format!("target/{target}/debug"),
235        "simple_tmk",
236        MissingCommand::Custom {
237            description: "simple_tmk",
238            cmd: &format!(
239                "RUSTC_BOOTSTRAP=1 cargo build -p simple_tmk --config openhcl/minimal_rt/{arch_str}-config.toml"
240            ),
241        },
242    )
243}
244
245/// Path to our packaged linux direct test kernel.
246fn linux_direct_x64_test_kernel_path() -> anyhow::Result<PathBuf> {
247    get_path(
248        ".packages/underhill-deps-private",
249        "x64/vmlinux",
250        MissingCommand::Restore {
251            description: "linux direct test kernel",
252        },
253    )
254}
255
256/// Path to our packaged linux direct test initrd.
257fn linux_direct_test_initrd_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
258    get_path(
259        ".packages/underhill-deps-private",
260        format!(
261            "{}/initrd",
262            match arch {
263                MachineArch::X86_64 => "x64",
264                MachineArch::Aarch64 => "aarch64",
265            }
266        ),
267        MissingCommand::Restore {
268            description: "linux direct test initrd",
269        },
270    )
271}
272
273/// Path to our packaged linux direct test kernel.
274fn linux_direct_arm_image_path() -> anyhow::Result<PathBuf> {
275    get_path(
276        ".packages/underhill-deps-private",
277        "aarch64/Image",
278        MissingCommand::Restore {
279            description: "linux direct test kernel",
280        },
281    )
282}
283
284/// Path to our packaged PCAT firmware.
285fn pcat_firmware_path() -> anyhow::Result<PathBuf> {
286    get_path(
287        ".packages",
288        "Microsoft.Windows.VmFirmware.Pcat.amd64fre/content/vmfirmwarepcat.dll",
289        MissingCommand::Restore {
290            description: "PCAT firmware binary",
291        },
292    )
293}
294
295/// Path to our packaged SVGA firmware.
296fn svga_firmware_path() -> anyhow::Result<PathBuf> {
297    get_path(
298        ".packages",
299        "Microsoft.Windows.VmEmulatedDevices.amd64fre/content/VmEmulatedDevices.dll",
300        MissingCommand::Restore {
301            description: "SVGA firmware binary",
302        },
303    )
304}
305
306/// Path to our packaged UEFI firmware image.
307fn uefi_firmware_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
308    get_path(
309        ".packages",
310        match arch {
311            MachineArch::X86_64 => {
312                "hyperv.uefi.mscoreuefi.x64.RELEASE/MsvmX64/RELEASE_VS2022/FV/MSVM.fd"
313            }
314            MachineArch::Aarch64 => {
315                "hyperv.uefi.mscoreuefi.AARCH64.RELEASE/MsvmAARCH64/RELEASE_VS2022/FV/MSVM.fd"
316            }
317        },
318        MissingCommand::Restore {
319            description: "UEFI firmware binary",
320        },
321    )
322}
323
324/// Path to the output location of the requested OpenHCL package.
325fn openhcl_bin_path(
326    arch: MachineArch,
327    version: OpenhclVersion,
328    flavor: OpenhclFlavor,
329) -> anyhow::Result<PathBuf> {
330    let (path, name, cmd) = match (arch, version, flavor) {
331        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Standard) => (
332            "flowey-out/artifacts/build-igvm/debug/x64",
333            "openhcl-x64.bin",
334            MissingCommand::XFlowey {
335                description: "OpenHCL IGVM file",
336                xflowey_args: &["build-igvm", "x64"],
337            },
338        ),
339        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel) => (
340            "flowey-out/artifacts/build-igvm/debug/x64-devkern",
341            "openhcl-x64-devkern.bin",
342            MissingCommand::XFlowey {
343                description: "OpenHCL IGVM file",
344                xflowey_args: &["build-igvm", "x64-devkern"],
345            },
346        ),
347        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Cvm) => (
348            "flowey-out/artifacts/build-igvm/debug/x64-cvm",
349            "openhcl-x64-cvm.bin",
350            MissingCommand::XFlowey {
351                description: "OpenHCL IGVM file",
352                xflowey_args: &["build-igvm", "x64-cvm"],
353            },
354        ),
355        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::LinuxDirect) => (
356            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
357            "openhcl-x64-test-linux-direct.bin",
358            MissingCommand::XFlowey {
359                description: "OpenHCL IGVM file",
360                xflowey_args: &["build-igvm", "x64-test-linux-direct"],
361            },
362        ),
363        (MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::Standard) => (
364            "flowey-out/artifacts/build-igvm/debug/aarch64",
365            "openhcl-aarch64.bin",
366            MissingCommand::XFlowey {
367                description: "OpenHCL IGVM file",
368                xflowey_args: &["build-igvm", "aarch64"],
369            },
370        ),
371        (MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel) => (
372            "flowey-out/artifacts/build-igvm/debug/aarch64-devkern",
373            "openhcl-aarch64-devkern.bin",
374            MissingCommand::XFlowey {
375                description: "OpenHCL IGVM file",
376                xflowey_args: &["build-igvm", "aarch64-devkern"],
377            },
378        ),
379        _ => anyhow::bail!("no openhcl bin with given arch, version, and flavor"),
380    };
381
382    get_path(path, name, cmd)
383}
384
385/// Path to the specified build artifact for the requested OpenHCL package.
386fn openhcl_extras_path(
387    version: OpenhclVersion,
388    flavor: OpenhclFlavor,
389    item: OpenhclExtras,
390) -> anyhow::Result<PathBuf> {
391    if !matches!(version, OpenhclVersion::Latest) || !matches!(flavor, OpenhclFlavor::LinuxDirect) {
392        anyhow::bail!("Debug symbol path currently only available for LATEST_LINUX_DIRECT_TEST")
393    }
394
395    let (path, name) = match item {
396        OpenhclExtras::UmBin => (
397            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
398            "openvmm_hcl_msft",
399        ),
400        OpenhclExtras::UmDbg => (
401            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
402            "openvmm_hcl_msft.dbg",
403        ),
404    };
405
406    get_path(
407        path,
408        name,
409        MissingCommand::XFlowey {
410            description: "OpenHCL IGVM file",
411            xflowey_args: &["build-igvm", "x64-test-linux-direct"],
412        },
413    )
414}
415
416/// Path to the per-test test output directory.
417fn test_log_directory_path(test_name: &str) -> anyhow::Result<PathBuf> {
418    let root = if let Some(path) = std::env::var_os("TEST_OUTPUT_PATH") {
419        PathBuf::from(path)
420    } else {
421        get_repo_root()?.join("vmm_test_results")
422    };
423    // Use a per-test subdirectory, replacing `::` with `__` to avoid issues
424    // with filesystems that don't support `::` in filenames.
425    let path = root.join(test_name.replace("::", "__"));
426    fs_err::create_dir_all(&path)?;
427    Ok(path)
428}
429
430const VMM_TESTS_DIR_ENV_VAR: &str = "VMM_TESTS_CONTENT_DIR";
431
432/// Gets a path to the root of the repo.
433pub fn get_repo_root() -> anyhow::Result<PathBuf> {
434    Ok(Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
435}
436
437/// Attempts to find the given file, first checking for it relative to the test
438/// content directory, then falling back to the provided search path.
439///
440/// Note that the file name can be a multi-segment path (e.g. `foo/bar.txt`) so
441/// that it must be in subdirectory of the test content directory. This is useful
442/// when multiple files with the same name are needed in different contexts.
443///
444/// If the search path is relative it is treated as relative to the repo root.
445/// If it is absolute it is used unchanged.
446///
447/// If the file cannot be found then the provided command will be returned as an
448/// easily printable error.
449// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
450pub fn get_path(
451    search_path: impl AsRef<Path>,
452    file_name: impl AsRef<Path>,
453    missing_cmd: MissingCommand<'_>,
454) -> anyhow::Result<PathBuf> {
455    let search_path = search_path.as_ref();
456    let file_name = file_name.as_ref();
457    if file_name.is_absolute() {
458        anyhow::bail!("{} should be a relative path", file_name.display());
459    }
460
461    if let Ok(env_dir) = std::env::var(VMM_TESTS_DIR_ENV_VAR) {
462        let full_path = Path::new(&env_dir).join(file_name);
463        if full_path.try_exists()? {
464            return Ok(full_path);
465        }
466    }
467
468    let file_path = if search_path.is_absolute() {
469        search_path.to_owned()
470    } else {
471        get_repo_root()?.join(search_path)
472    };
473
474    let full_path = file_path.join(file_name);
475    if !full_path.exists() {
476        eprintln!("Failed to find {:?}.", full_path);
477        missing_cmd.to_error()?;
478    }
479
480    Ok(full_path)
481}
482
483/// Attempts to find the path to a rust executable built by Cargo, checking
484/// the test content directory if the environment variable is set.
485// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
486pub fn get_output_executable_path(name: &str) -> anyhow::Result<PathBuf> {
487    let mut path: PathBuf = std::env::current_exe()?;
488    // Sometimes we end up inside deps instead of the output dir, but if we
489    // are we can just go up a level.
490    if path.parent().and_then(|x| x.file_name()).unwrap() == "deps" {
491        path.pop();
492    }
493
494    get_path(
495        path.parent().unwrap(),
496        Path::new(name).with_extension(EXE_EXTENSION),
497        MissingCommand::Build {
498            package: name,
499            target: None,
500        },
501    )
502}
503
504/// A description of a command that can be run to create a missing file.
505// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
506#[derive(Copy, Clone)]
507#[expect(missing_docs)] // Self-describing field names.
508pub enum MissingCommand<'a> {
509    /// A `cargo build` invocation.
510    Build {
511        package: &'a str,
512        target: Option<&'a str>,
513    },
514    /// A `cargo xtask` invocation.
515    Xtask {
516        description: &'a str,
517        xtask_args: &'a [&'a str],
518    },
519    /// A `cargo xflowey` invocation.
520    XFlowey {
521        description: &'a str,
522        xflowey_args: &'a [&'a str],
523    },
524    /// A `xflowey restore-packages` invocation.
525    Restore { description: &'a str },
526    /// A custom command.
527    Custom { description: &'a str, cmd: &'a str },
528}
529
530impl MissingCommand<'_> {
531    fn to_error(self) -> anyhow::Result<()> {
532        match self {
533            MissingCommand::Build { package, target } => anyhow::bail!(
534                "Failed to find {package} binary. Run `cargo build {target_args}-p {package}` to build it.",
535                target_args =
536                    target.map_or(String::new(), |target| format!("--target {} ", target)),
537            ),
538            MissingCommand::Xtask {
539                description,
540                xtask_args: args,
541            } => {
542                anyhow::bail!(
543                    "Failed to find {}. Run `cargo xtask {}` to create it.",
544                    description,
545                    args.join(" ")
546                )
547            }
548            MissingCommand::XFlowey {
549                description,
550                xflowey_args: args,
551            } => anyhow::bail!(
552                "Failed to find {}. Run `cargo xflowey {}` to create it.",
553                description,
554                args.join(" ")
555            ),
556            MissingCommand::Restore { description } => {
557                anyhow::bail!(
558                    "Failed to find {}. Run `cargo xflowey restore-packages`.",
559                    description
560                )
561            }
562            MissingCommand::Custom { description, cmd } => {
563                anyhow::bail!(
564                    "Failed to find {}. Run `{}` to create it.",
565                    description,
566                    cmd
567                )
568            }
569        }
570    }
571}