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::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
122/// The architecture specific fragment of the name of the directory used by rust when referring to specific targets.
123fn 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
164/// Path to the output location of our guest-test image for UEFI.
165fn guest_test_uefi_disk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
166    // `guest_test_uefi` is always at `{arch}-unknown-uefi/debug`
167    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
187/// Path to the output location of the pipette executable.
188fn pipette_path(arch: MachineArch, os_flavor: PipetteFlavor) -> anyhow::Result<PathBuf> {
189    // Always use (statically-built) musl on Linux to avoid needing libc
190    // compatibility.
191    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
222/// Path to the output location of the openvmm executable.
223fn openvmm_native_executable_path() -> anyhow::Result<PathBuf> {
224    get_output_executable_path("openvmm")
225}
226
227/// Path to the output location of the tmk_vmm executable.
228fn tmk_vmm_native_executable_path() -> anyhow::Result<PathBuf> {
229    get_output_executable_path("tmk_vmm")
230}
231
232/// Path to the output location of the vmgstool executable.
233fn 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
252/// Path to the output location of the simple_tmk executable.
253fn 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
274/// Path to our packaged linux direct test kernel.
275fn 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
285/// Path to our packaged linux direct test initrd.
286fn 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
302/// Path to our packaged linux direct test kernel.
303fn 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
313/// Path to our packaged PCAT firmware.
314fn 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
324/// Path to our packaged SVGA firmware.
325fn 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
335/// Path to our packaged UEFI firmware image.
336fn 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
353/// Path to the output location of the requested OpenHCL package.
354fn 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
438/// Path to the specified build artifact for the requested OpenHCL package.
439fn 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
469/// Path to the per-test test output directory.
470fn 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    // Use a per-test subdirectory, replacing `::` with `__` to avoid issues
477    // with filesystems that don't support `::` in filenames.
478    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
485/// Gets a path to the root of the repo.
486pub fn get_repo_root() -> anyhow::Result<PathBuf> {
487    Ok(Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
488}
489
490/// Attempts to find the given file, first checking for it relative to the test
491/// content directory, then falling back to the provided search path.
492///
493/// Note that the file name can be a multi-segment path (e.g. `foo/bar.txt`) so
494/// that it must be in subdirectory of the test content directory. This is useful
495/// when multiple files with the same name are needed in different contexts.
496///
497/// If the search path is relative it is treated as relative to the repo root.
498/// If it is absolute it is used unchanged.
499///
500/// If the file cannot be found then the provided command will be returned as an
501/// easily printable error.
502// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
503pub 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
536/// Attempts to find the path to a rust executable built by Cargo, checking
537/// the test content directory if the environment variable is set.
538// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
539pub fn get_output_executable_path(name: &str) -> anyhow::Result<PathBuf> {
540    let mut path: PathBuf = std::env::current_exe()?;
541    // Sometimes we end up inside deps instead of the output dir, but if we
542    // are we can just go up a level.
543    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/// A description of a command that can be run to create a missing file.
558// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
559#[derive(Copy, Clone)]
560#[expect(missing_docs)] // Self-describing field names.
561pub enum MissingCommand<'a> {
562    /// A `cargo build` invocation.
563    Build {
564        package: &'a str,
565        target: Option<&'a str>,
566    },
567    /// A `cargo run` invocation.
568    Run {
569        description: &'a str,
570        package: &'a str,
571    },
572    /// A `cargo xtask` invocation.
573    Xtask {
574        description: &'a str,
575        xtask_args: &'a [&'a str],
576    },
577    /// A `cargo xflowey` invocation.
578    XFlowey {
579        description: &'a str,
580        xflowey_args: &'a [&'a str],
581    },
582    /// A `xflowey restore-packages` invocation.
583    Restore { description: &'a str },
584    /// A custom command.
585    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}