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