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