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