Skip to main content

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::ArtifactSource;
10use petri_artifacts_core::AsArtifactHandle;
11use petri_artifacts_core::ErasedArtifactHandle;
12use std::env::consts::EXE_EXTENSION;
13use std::path::Path;
14use std::path::PathBuf;
15use vmm_test_images::CONTAINER;
16use vmm_test_images::KnownTestArtifacts;
17use vmm_test_images::STORAGE_ACCOUNT;
18
19/// Returns the Cargo build profile directory name for cross-compiled
20/// artifacts (e.g., pipette).
21///
22/// Infers the profile from the currently running binary's path (looking
23/// for a `release` component in the executable path). Defaults to `"debug"`.
24// DEVNOTE: `pub` in order to re-use in perf_tests and other crates.
25pub fn cargo_build_profile() -> &'static str {
26    static PROFILE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
27    PROFILE.get_or_init(|| {
28        if let Ok(exe) = std::env::current_exe() {
29            if exe.components().any(|c| c.as_os_str() == "release") {
30                return "release".to_string();
31            }
32        }
33        "debug".to_string()
34    })
35}
36
37/// An implementation of [`petri_artifacts_core::ResolveTestArtifact`]
38/// that resolves artifacts to various "known paths" within the context of
39/// the OpenVMM repository.
40pub struct OpenvmmKnownPathsTestArtifactResolver<'a>(&'a str);
41
42impl<'a> OpenvmmKnownPathsTestArtifactResolver<'a> {
43    /// Creates a new resolver for a test with the given name.
44    pub fn new(test_name: &'a str) -> Self {
45        Self(test_name)
46    }
47}
48
49impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifactResolver<'_> {
50    #[rustfmt::skip]
51    fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
52        use petri_artifacts_common::artifacts as common;
53        use petri_artifacts_vmm_test::artifacts::*;
54        use petri_artifacts_vmm_test::tags::IsHostedOnHvliteAzureBlobStore;
55
56        match id {
57            _ if id == common::PIPETTE_WINDOWS_X64 => pipette_path(MachineArch::X86_64, PipetteFlavor::Windows),
58            _ if id == common::PIPETTE_LINUX_X64 => pipette_path(MachineArch::X86_64, PipetteFlavor::Linux),
59            _ if id == common::PIPETTE_WINDOWS_AARCH64 => pipette_path(MachineArch::Aarch64, PipetteFlavor::Windows),
60            _ if id == common::PIPETTE_LINUX_AARCH64 => pipette_path(MachineArch::Aarch64, PipetteFlavor::Linux),
61
62            _ if id == common::TEST_LOG_DIRECTORY => test_log_directory_path(self.0),
63
64            _ if id == OPENVMM_NATIVE => openvmm_native_executable_path(),
65            #[cfg(target_os = "linux")]
66            _ if id == OPENVMM_VHOST_NATIVE => openvmm_vhost_native_executable_path(),
67
68            _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_X64 => linux_direct_x64_test_kernel_path(),
69            _ if id == loadable::LINUX_DIRECT_TEST_BZIMAGE_X64 => linux_direct_x64_test_bzimage_path(),
70            _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64 => linux_direct_arm_image_path(),
71            _ if id == loadable::LINUX_DIRECT_TEST_INITRD_X64 => linux_direct_test_initrd_path(MachineArch::X86_64),
72            _ if id == loadable::LINUX_DIRECT_TEST_INITRD_AARCH64 => linux_direct_test_initrd_path(MachineArch::Aarch64),
73
74            _ if id == petritools::PETRITOOLS_EROFS_X64 => petritools_erofs_path(MachineArch::X86_64),
75            _ if id == petritools::PETRITOOLS_EROFS_AARCH64 => petritools_erofs_path(MachineArch::Aarch64),
76
77            _ if id == loadable::PCAT_FIRMWARE_X64 => pcat_firmware_path(),
78            _ if id == loadable::SVGA_FIRMWARE_X64 => svga_firmware_path(),
79            _ if id == loadable::UEFI_FIRMWARE_X64 => uefi_firmware_path(MachineArch::X86_64),
80            _ if id == loadable::UEFI_FIRMWARE_AARCH64 => uefi_firmware_path(MachineArch::Aarch64),
81
82            _ if id == openhcl_igvm::LATEST_STANDARD_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Standard),
83            _ if id == openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel),
84            _ if id == openhcl_igvm::LATEST_CVM_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Cvm),
85            _ if id == openhcl_igvm::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::LinuxDirect),
86            _ if id == openhcl_igvm::LATEST_STANDARD_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::Standard),
87            _ if id == openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel),
88
89            _ if id == openhcl_igvm::LATEST_RELEASE_STANDARD_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Release2511, OpenhclFlavor::Standard),
90            _ if id == openhcl_igvm::LATEST_RELEASE_LINUX_DIRECT_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Release2511, OpenhclFlavor::LinuxDirect),
91            _ if id == openhcl_igvm::LATEST_RELEASE_STANDARD_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Release2511, OpenhclFlavor::Standard),
92
93            _ if id == openhcl_igvm::um_bin::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_extras_path(OpenhclVersion::Latest,OpenhclFlavor::LinuxDirect,OpenhclExtras::UmBin),
94            _ if id == openhcl_igvm::um_dbg::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_extras_path(OpenhclVersion::Latest,OpenhclFlavor::LinuxDirect,OpenhclExtras::UmDbg),
95
96            _ if id == test_vhd::GUEST_TEST_UEFI_X64 => guest_test_uefi_disk_path(MachineArch::X86_64),
97            _ if id == test_vhd::GUEST_TEST_UEFI_AARCH64 => guest_test_uefi_disk_path(MachineArch::Aarch64),
98
99            _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64_PREPPED => {
100                let base_filename = test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64::FILENAME;
101                let prepped_filename = base_filename.replace(".vhd", "-prepped.vhd");
102                let images_dir = std::env::var("VMM_TEST_IMAGES");
103                let full_path = Path::new(images_dir.as_deref().unwrap_or("images"));
104                get_path(
105                    full_path,
106                    prepped_filename,
107                    MissingCommand::Run {
108                        description: "prepped test image",
109                        package: "prep_steps",
110                    },
111                )
112            }
113
114            _ if id == tmks::TMK_VMM_NATIVE => tmk_vmm_native_executable_path(),
115            _ if id == tmks::TMK_VMM_LINUX_X64_MUSL => tmk_vmm_paravisor_path(MachineArch::X86_64),
116            _ if id == tmks::TMK_VMM_LINUX_AARCH64_MUSL => tmk_vmm_paravisor_path(MachineArch::Aarch64),
117            _ if id == tmks::SIMPLE_TMK_X64 => simple_tmk_path(MachineArch::X86_64),
118            _ if id == tmks::SIMPLE_TMK_AARCH64 => simple_tmk_path(MachineArch::Aarch64),
119
120            _ if id == VMGSTOOL_NATIVE => vmgstool_native_executable_path(),
121
122            _ if id == guest_tools::TPM_GUEST_TESTS_WINDOWS_X64 => {
123                tpm_guest_tests_windows_path(MachineArch::X86_64)
124            }
125            _ if id == guest_tools::TPM_GUEST_TESTS_LINUX_X64 => {
126                tpm_guest_tests_linux_path(MachineArch::X86_64)
127            }
128
129            _ if id == host_tools::TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64 => {
130                test_igvm_agent_rpc_server_windows_path(MachineArch::X86_64)
131            }
132
133            // Blob-hosted artifacts: resolved via blob_artifact_info.
134            _ if let Some(artifact) = KnownTestArtifacts::from_handle(id) => {
135                get_test_artifact_path(artifact)
136            }
137
138            _ => anyhow::bail!("no support for given artifact type"),
139        }
140    }
141
142    fn resolve_source(&self, id: ErasedArtifactHandle) -> anyhow::Result<ArtifactSource> {
143        // Try local resolution first.
144        let local_err = match self.resolve(id) {
145            Ok(path) => return Ok(ArtifactSource::Local(path)),
146            Err(e) => e,
147        };
148
149        // Fall back to remote URL for artifacts hosted on Azure Blob Storage,
150        // but only for formats the blob disk backend supports (fixed VHD1 and flat).
151        if let Some(artifact) = KnownTestArtifacts::from_handle(id) {
152            if artifact.supports_blob_disk() {
153                let url = format!(
154                    "https://{STORAGE_ACCOUNT}.blob.core.windows.net/{CONTAINER}/{}",
155                    artifact.filename()
156                );
157                return Ok(ArtifactSource::Remote { url });
158            }
159        }
160
161        // No local path and no remote URL available — return the original error.
162        Err(local_err)
163    }
164}
165
166/// Returns the bundle-relative file name for the given artifact.
167///
168/// This is the `file_name` argument that [`get_path`] would use when
169/// resolving this artifact. When creating a self-contained bundle for
170/// deployment, place the artifact at this relative path within the
171/// bundle directory, then set `VMM_TESTS_CONTENT_DIR` to the bundle
172/// directory at runtime.
173///
174/// Returns `None` for artifacts that don't have a fixed bundle name
175/// (e.g., log directories).
176pub fn resolve_bundle_name(id: ErasedArtifactHandle) -> Option<&'static str> {
177    use petri_artifacts_common::artifacts as common;
178    use petri_artifacts_vmm_test::artifacts::*;
179
180    match id {
181        _ if id == common::PIPETTE_LINUX_X64 => Some("pipette"),
182        _ if id == common::PIPETTE_LINUX_AARCH64 => Some("pipette"),
183        _ if id == common::PIPETTE_WINDOWS_X64 => Some("pipette.exe"),
184        _ if id == common::PIPETTE_WINDOWS_AARCH64 => Some("pipette.exe"),
185        _ if id == OPENVMM_NATIVE => Some(if cfg!(windows) {
186            "openvmm.exe"
187        } else {
188            "openvmm"
189        }),
190        _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_X64 => Some("x64/vmlinux"),
191        _ if id == loadable::LINUX_DIRECT_TEST_BZIMAGE_X64 => Some("x64/bzImage"),
192        _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64 => Some("aarch64/Image"),
193        _ if id == loadable::LINUX_DIRECT_TEST_INITRD_X64 => Some("x64/initrd"),
194        _ if id == loadable::LINUX_DIRECT_TEST_INITRD_AARCH64 => Some("aarch64/initrd"),
195        _ if id == petritools::PETRITOOLS_EROFS_X64 => Some("x64/petritools.erofs"),
196        _ if id == petritools::PETRITOOLS_EROFS_AARCH64 => Some("aarch64/petritools.erofs"),
197        _ if id == loadable::UEFI_FIRMWARE_X64 => {
198            Some("hyperv.uefi.mscoreuefi.x64.RELEASE/MsvmX64/RELEASE_VS2022/FV/MSVM.fd")
199        }
200        _ if id == loadable::UEFI_FIRMWARE_AARCH64 => {
201            Some("hyperv.uefi.mscoreuefi.AARCH64.RELEASE/MsvmAARCH64/RELEASE_CLANGPDB/FV/MSVM.fd")
202        }
203        _ => {
204            // For test VHDs, the bundle name is the artifact filename from
205            // IsHostedOnHvliteAzureBlobStore. Use resolve_test_vhd_bundle_name
206            // for those.
207            None
208        }
209    }
210}
211
212#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
213enum PipetteFlavor {
214    Windows,
215    Linux,
216}
217
218#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
219enum OpenhclVersion {
220    Latest,
221    Release2511,
222}
223
224#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
225enum OpenhclFlavor {
226    Standard,
227    StandardDevKernel,
228    Cvm,
229    LinuxDirect,
230}
231
232#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
233enum OpenhclExtras {
234    UmBin,
235    UmDbg,
236}
237
238/// The architecture specific fragment of the name of the directory used by rust when referring to specific targets.
239fn target_arch_path(arch: MachineArch) -> &'static str {
240    match arch {
241        MachineArch::X86_64 => "x86_64",
242        MachineArch::Aarch64 => "aarch64",
243    }
244}
245
246fn windows_msvc_target(arch: MachineArch) -> &'static str {
247    match arch {
248        MachineArch::X86_64 => "x86_64-pc-windows-msvc",
249        MachineArch::Aarch64 => "aarch64-pc-windows-msvc",
250    }
251}
252
253fn get_test_artifact_path(artifact: KnownTestArtifacts) -> Result<PathBuf, anyhow::Error> {
254    let images_dir = std::env::var("VMM_TEST_IMAGES");
255    let full_path = Path::new(images_dir.as_deref().unwrap_or("images"));
256
257    get_path(
258        full_path,
259        artifact.filename(),
260        MissingCommand::Xtask {
261            xtask_args: &[
262                "guest-test",
263                "download-image",
264                "--artifacts",
265                artifact.name(),
266            ],
267            description: "test artifact",
268        },
269    )
270}
271
272/// Path to the output location of our guest-test image for UEFI.
273fn guest_test_uefi_disk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
274    // `guest_test_uefi` is always at `{arch}-unknown-uefi/debug`
275    get_path(
276        format!("target/{}-unknown-uefi/debug", target_arch_path(arch)),
277        "guest_test_uefi.img",
278        MissingCommand::Xtask {
279            xtask_args: &[
280                "guest-test",
281                "uefi",
282                &format!(
283                    "--boot{}",
284                    match arch {
285                        MachineArch::X86_64 => "x64",
286                        MachineArch::Aarch64 => "aa64",
287                    }
288                ),
289            ],
290            description: "guest_test_uefi image",
291        },
292    )
293}
294
295/// Path to the output location of the pipette executable.
296fn pipette_path(arch: MachineArch, os_flavor: PipetteFlavor) -> anyhow::Result<PathBuf> {
297    // Always use (statically-built) musl on Linux to avoid needing libc
298    // compatibility.
299    let (target_suffixes, binary) = match os_flavor {
300        PipetteFlavor::Windows => (vec!["pc-windows-msvc", "pc-windows-gnu"], "pipette.exe"),
301        PipetteFlavor::Linux => (vec!["unknown-linux-musl"], "pipette"),
302    };
303    for (index, target_suffix) in target_suffixes.iter().enumerate() {
304        let target = format!("{}-{}", target_arch_path(arch), target_suffix);
305        match get_path(
306            format!("target/{target}/{}", cargo_build_profile()),
307            binary,
308            MissingCommand::Build {
309                package: "pipette",
310                target: Some(&target),
311            },
312        ) {
313            Ok(path) => return Ok(path),
314            Err(err) => {
315                if index < target_suffixes.len() - 1 {
316                    continue;
317                } else {
318                    anyhow::bail!(
319                        "None of the suffixes {:?} had `pipette` built, {err:?}",
320                        target_suffixes
321                    );
322                }
323            }
324        }
325    }
326
327    unreachable!()
328}
329
330/// Path to the output location of the openvmm executable.
331fn openvmm_native_executable_path() -> anyhow::Result<PathBuf> {
332    get_output_executable_path("openvmm")
333}
334
335/// Path to the output location of the openvmm_vhost executable.
336#[cfg(target_os = "linux")]
337fn openvmm_vhost_native_executable_path() -> anyhow::Result<PathBuf> {
338    get_output_executable_path("openvmm_vhost")
339}
340
341/// Path to the output location of the tmk_vmm executable.
342fn tmk_vmm_native_executable_path() -> anyhow::Result<PathBuf> {
343    get_output_executable_path("tmk_vmm")
344}
345
346/// Path to the output location of the vmgstool executable.
347fn vmgstool_native_executable_path() -> anyhow::Result<PathBuf> {
348    get_output_executable_path("vmgstool")
349}
350
351/// Path to the output location of the tpm_guest_tests executable.
352fn tpm_guest_tests_windows_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
353    let target = windows_msvc_target(arch);
354    get_path(
355        format!("target/{target}/debug"),
356        "tpm_guest_tests.exe",
357        MissingCommand::Build {
358            package: "tpm_guest_tests",
359            target: Some(target),
360        },
361    )
362}
363
364fn tpm_guest_tests_linux_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
365    let target = match arch {
366        MachineArch::X86_64 => "x86_64-unknown-linux-gnu",
367        MachineArch::Aarch64 => "aarch64-unknown-linux-gnu",
368    };
369
370    get_path(
371        format!("target/{target}/debug"),
372        "tpm_guest_tests",
373        MissingCommand::Build {
374            package: "tpm_guest_tests",
375            target: Some(target),
376        },
377    )
378}
379
380/// Path to the output location of the test_igvm_agent_rpc_server executable.
381fn test_igvm_agent_rpc_server_windows_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
382    let target = windows_msvc_target(arch);
383    get_path(
384        format!("target/{target}/debug"),
385        "test_igvm_agent_rpc_server.exe",
386        MissingCommand::Build {
387            package: "test_igvm_agent_rpc_server",
388            target: Some(target),
389        },
390    )
391}
392
393fn tmk_vmm_paravisor_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
394    let target = match arch {
395        MachineArch::X86_64 => "x86_64-unknown-linux-musl",
396        MachineArch::Aarch64 => "aarch64-unknown-linux-musl",
397    };
398    get_path(
399        format!("target/{target}/debug"),
400        "tmk_vmm",
401        MissingCommand::Build {
402            package: "tmk_vmm",
403            target: Some(target),
404        },
405    )
406}
407
408/// Path to the output location of the simple_tmk executable.
409fn simple_tmk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
410    let arch_str = match arch {
411        MachineArch::X86_64 => "x86_64",
412        MachineArch::Aarch64 => "aarch64",
413    };
414    let target = match arch {
415        MachineArch::X86_64 => "x86_64-unknown-none",
416        MachineArch::Aarch64 => "aarch64-minimal_rt-none",
417    };
418    get_path(
419        format!("target/{target}/debug"),
420        "simple_tmk",
421        MissingCommand::Custom {
422            description: "simple_tmk",
423            cmd: &format!(
424                "RUSTC_BOOTSTRAP=1 cargo build -p simple_tmk --config openhcl/minimal_rt/{arch_str}-config.toml"
425            ),
426        },
427    )
428}
429
430/// Path to our packaged linux direct test kernel.
431fn linux_direct_x64_test_kernel_path() -> anyhow::Result<PathBuf> {
432    use petri_artifacts_vmm_test::artifacts::loadable;
433    get_path(
434        ".packages/underhill-deps-private",
435        resolve_bundle_name(loadable::LINUX_DIRECT_TEST_KERNEL_X64.erase()).unwrap(),
436        MissingCommand::Restore {
437            description: "linux direct test kernel",
438        },
439    )
440}
441
442/// Path to our packaged linux direct test bzImage.
443fn linux_direct_x64_test_bzimage_path() -> anyhow::Result<PathBuf> {
444    use petri_artifacts_vmm_test::artifacts::loadable;
445    get_path(
446        ".packages/underhill-deps-private",
447        resolve_bundle_name(loadable::LINUX_DIRECT_TEST_BZIMAGE_X64.erase()).unwrap(),
448        MissingCommand::Restore {
449            description: "linux direct test bzImage",
450        },
451    )
452}
453
454/// Path to our packaged linux direct test initrd.
455fn linux_direct_test_initrd_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
456    use petri_artifacts_vmm_test::artifacts::loadable;
457    let id = match arch {
458        MachineArch::X86_64 => loadable::LINUX_DIRECT_TEST_INITRD_X64.erase(),
459        MachineArch::Aarch64 => loadable::LINUX_DIRECT_TEST_INITRD_AARCH64.erase(),
460    };
461    get_path(
462        ".packages/underhill-deps-private",
463        resolve_bundle_name(id).unwrap(),
464        MissingCommand::Restore {
465            description: "linux direct test initrd",
466        },
467    )
468}
469
470/// Path to our packaged petritools erofs image.
471fn petritools_erofs_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
472    use petri_artifacts_vmm_test::artifacts::petritools;
473    let id = match arch {
474        MachineArch::X86_64 => petritools::PETRITOOLS_EROFS_X64.erase(),
475        MachineArch::Aarch64 => petritools::PETRITOOLS_EROFS_AARCH64.erase(),
476    };
477    get_path(
478        ".packages/underhill-deps-private",
479        resolve_bundle_name(id).unwrap(),
480        MissingCommand::Restore {
481            description: "petritools erofs image",
482        },
483    )
484}
485
486/// Path to our packaged linux direct test kernel.
487fn linux_direct_arm_image_path() -> anyhow::Result<PathBuf> {
488    use petri_artifacts_vmm_test::artifacts::loadable;
489    get_path(
490        ".packages/underhill-deps-private",
491        resolve_bundle_name(loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64.erase()).unwrap(),
492        MissingCommand::Restore {
493            description: "linux direct test kernel",
494        },
495    )
496}
497
498/// Path to our packaged PCAT firmware.
499fn pcat_firmware_path() -> anyhow::Result<PathBuf> {
500    get_path(
501        ".packages",
502        "Microsoft.Windows.VmFirmware.Pcat.amd64fre/content/vmfirmwarepcat.dll",
503        MissingCommand::Restore {
504            description: "PCAT firmware binary",
505        },
506    )
507}
508
509/// Path to our packaged SVGA firmware.
510fn svga_firmware_path() -> anyhow::Result<PathBuf> {
511    get_path(
512        ".packages",
513        "Microsoft.Windows.VmEmulatedDevices.amd64fre/content/VmEmulatedDevices.dll",
514        MissingCommand::Restore {
515            description: "SVGA firmware binary",
516        },
517    )
518}
519
520/// Path to our packaged UEFI firmware image.
521fn uefi_firmware_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
522    use petri_artifacts_vmm_test::artifacts::loadable;
523    let id = match arch {
524        MachineArch::X86_64 => loadable::UEFI_FIRMWARE_X64.erase(),
525        MachineArch::Aarch64 => loadable::UEFI_FIRMWARE_AARCH64.erase(),
526    };
527    get_path(
528        ".packages",
529        resolve_bundle_name(id).unwrap(),
530        MissingCommand::Restore {
531            description: "UEFI firmware binary",
532        },
533    )
534}
535
536/// Path to the output location of the requested OpenHCL package.
537fn openhcl_bin_path(
538    arch: MachineArch,
539    version: OpenhclVersion,
540    flavor: OpenhclFlavor,
541) -> anyhow::Result<PathBuf> {
542    let (path, name, cmd) = match (arch, version, flavor) {
543        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Standard) => (
544            "flowey-out/artifacts/build-igvm/debug/x64",
545            "openhcl-x64.bin",
546            MissingCommand::XFlowey {
547                description: "OpenHCL IGVM file",
548                xflowey_args: &["build-igvm", "x64"],
549            },
550        ),
551        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel) => (
552            "flowey-out/artifacts/build-igvm/debug/x64-devkern",
553            "openhcl-x64-devkern.bin",
554            MissingCommand::XFlowey {
555                description: "OpenHCL IGVM file",
556                xflowey_args: &["build-igvm", "x64-devkern"],
557            },
558        ),
559        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Cvm) => (
560            "flowey-out/artifacts/build-igvm/debug/x64-cvm",
561            "openhcl-x64-cvm.bin",
562            MissingCommand::XFlowey {
563                description: "OpenHCL IGVM file",
564                xflowey_args: &["build-igvm", "x64-cvm"],
565            },
566        ),
567        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::LinuxDirect) => (
568            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
569            "openhcl-x64-test-linux-direct.bin",
570            MissingCommand::XFlowey {
571                description: "OpenHCL IGVM file",
572                xflowey_args: &["build-igvm", "x64-test-linux-direct"],
573            },
574        ),
575        (MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::Standard) => (
576            "flowey-out/artifacts/build-igvm/debug/aarch64",
577            "openhcl-aarch64.bin",
578            MissingCommand::XFlowey {
579                description: "OpenHCL IGVM file",
580                xflowey_args: &["build-igvm", "aarch64"],
581            },
582        ),
583        (MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel) => (
584            "flowey-out/artifacts/build-igvm/debug/aarch64-devkern",
585            "openhcl-aarch64-devkern.bin",
586            MissingCommand::XFlowey {
587                description: "OpenHCL IGVM file",
588                xflowey_args: &["build-igvm", "aarch64-devkern"],
589            },
590        ),
591        (MachineArch::X86_64, OpenhclVersion::Release2511, OpenhclFlavor::LinuxDirect) => (
592            "flowey-out/artifacts/last-release-igvm-files",
593            "release-2511-x64-direct-openhcl.bin",
594            MissingCommand::XFlowey {
595                description: "Previous OpenHCL release IGVM file",
596                xflowey_args: &["restore-packages"],
597            },
598        ),
599        (MachineArch::X86_64, OpenhclVersion::Release2511, OpenhclFlavor::Standard) => (
600            "flowey-out/artifacts/last-release-igvm-files",
601            "release-2511-x64-openhcl.bin",
602            MissingCommand::XFlowey {
603                description: "Previous OpenHCL release IGVM file",
604                xflowey_args: &["restore-packages"],
605            },
606        ),
607        (MachineArch::Aarch64, OpenhclVersion::Release2511, OpenhclFlavor::Standard) => (
608            "flowey-out/artifacts/last-release-igvm-files",
609            "release-2511-aarch64-openhcl.bin",
610            MissingCommand::XFlowey {
611                description: "Previous OpenHCL release IGVM file",
612                xflowey_args: &["restore-packages"],
613            },
614        ),
615        _ => anyhow::bail!("no openhcl bin with given arch, version, and flavor"),
616    };
617
618    get_path(path, name, cmd)
619}
620
621/// Path to the specified build artifact for the requested OpenHCL package.
622fn openhcl_extras_path(
623    version: OpenhclVersion,
624    flavor: OpenhclFlavor,
625    item: OpenhclExtras,
626) -> anyhow::Result<PathBuf> {
627    if !matches!(version, OpenhclVersion::Latest) || !matches!(flavor, OpenhclFlavor::LinuxDirect) {
628        anyhow::bail!("Debug symbol path currently only available for LATEST_LINUX_DIRECT_TEST")
629    }
630
631    let (path, name) = match item {
632        OpenhclExtras::UmBin => (
633            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
634            "openvmm_hcl_msft",
635        ),
636        OpenhclExtras::UmDbg => (
637            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
638            "openvmm_hcl_msft.dbg",
639        ),
640    };
641
642    get_path(
643        path,
644        name,
645        MissingCommand::XFlowey {
646            description: "OpenHCL IGVM file",
647            xflowey_args: &["build-igvm", "x64-test-linux-direct"],
648        },
649    )
650}
651
652/// Path to the per-test test output directory.
653fn test_log_directory_path(test_name: &str) -> anyhow::Result<PathBuf> {
654    let root = if let Some(path) = std::env::var_os("TEST_OUTPUT_PATH") {
655        PathBuf::from(path)
656    } else {
657        get_repo_root()?.join("vmm_test_results")
658    };
659    // Use a per-test subdirectory, replacing `::` with `__` to avoid issues
660    // with filesystems that don't support `::` in filenames.
661    let path = root.join(test_name.replace("::", "__"));
662    fs_err::create_dir_all(&path)?;
663    Ok(path)
664}
665
666const VMM_TESTS_DIR_ENV_VAR: &str = "VMM_TESTS_CONTENT_DIR";
667
668/// Gets a path to the root of the repo.
669pub fn get_repo_root() -> anyhow::Result<PathBuf> {
670    Ok(Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
671}
672
673/// Attempts to find the given file, first checking for it relative to the test
674/// content directory, then falling back to the provided search path.
675///
676/// Note that the file name can be a multi-segment path (e.g. `foo/bar.txt`) so
677/// that it must be in subdirectory of the test content directory. This is useful
678/// when multiple files with the same name are needed in different contexts.
679///
680/// If the search path is relative it is treated as relative to the repo root.
681/// If it is absolute it is used unchanged.
682///
683/// If the file cannot be found then the provided command will be returned as an
684/// easily printable error.
685// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
686pub fn get_path(
687    search_path: impl AsRef<Path>,
688    file_name: impl AsRef<Path>,
689    missing_cmd: MissingCommand<'_>,
690) -> anyhow::Result<PathBuf> {
691    let search_path = search_path.as_ref();
692    let file_name = file_name.as_ref();
693    if file_name.is_absolute() {
694        anyhow::bail!("{} should be a relative path", file_name.display());
695    }
696
697    if let Ok(env_dir) = std::env::var(VMM_TESTS_DIR_ENV_VAR) {
698        let full_path = Path::new(&env_dir).join(file_name);
699        if full_path.try_exists()? {
700            return Ok(full_path);
701        }
702    }
703
704    let file_path = if search_path.is_absolute() {
705        search_path.to_owned()
706    } else {
707        get_repo_root()?.join(search_path)
708    };
709
710    let full_path = file_path.join(file_name);
711    if !full_path.exists() {
712        eprintln!("Failed to find {:?}.", full_path);
713        missing_cmd.to_error()?;
714    }
715
716    Ok(full_path)
717}
718
719/// Attempts to find the path to a rust executable built by Cargo, checking
720/// the test content directory if the environment variable is set.
721// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
722pub fn get_output_executable_path(name: &str) -> anyhow::Result<PathBuf> {
723    let mut path: PathBuf = std::env::current_exe()?;
724    // Sometimes we end up inside deps instead of the output dir, but if we
725    // are we can just go up a level.
726    if path.parent().and_then(|x| x.file_name()).unwrap() == "deps" {
727        path.pop();
728    }
729
730    get_path(
731        path.parent().unwrap(),
732        Path::new(name).with_extension(EXE_EXTENSION),
733        MissingCommand::Build {
734            package: name,
735            target: None,
736        },
737    )
738}
739
740/// A description of a command that can be run to create a missing file.
741// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
742#[derive(Copy, Clone)]
743#[expect(missing_docs)] // Self-describing field names.
744pub enum MissingCommand<'a> {
745    /// A `cargo build` invocation.
746    Build {
747        package: &'a str,
748        target: Option<&'a str>,
749    },
750    /// A `cargo run` invocation.
751    Run {
752        description: &'a str,
753        package: &'a str,
754    },
755    /// A `cargo xtask` invocation.
756    Xtask {
757        description: &'a str,
758        xtask_args: &'a [&'a str],
759    },
760    /// A `cargo xflowey` invocation.
761    XFlowey {
762        description: &'a str,
763        xflowey_args: &'a [&'a str],
764    },
765    /// A `xflowey restore-packages` invocation.
766    Restore { description: &'a str },
767    /// A custom command.
768    Custom { description: &'a str, cmd: &'a str },
769}
770
771impl MissingCommand<'_> {
772    fn to_error(self) -> anyhow::Result<()> {
773        match self {
774            MissingCommand::Build { package, target } => anyhow::bail!(
775                "Failed to find {package} binary. Run `cargo build {target_args}-p {package}` to build it.",
776                target_args =
777                    target.map_or(String::new(), |target| format!("--target {} ", target)),
778            ),
779            MissingCommand::Run {
780                description,
781                package,
782            } => anyhow::bail!(
783                "Failed to find {}. Run `cargo run -p {}` to create it.",
784                description,
785                package
786            ),
787            MissingCommand::Xtask {
788                description,
789                xtask_args: args,
790            } => {
791                anyhow::bail!(
792                    "Failed to find {}. Run `cargo xtask {}` to create it.",
793                    description,
794                    args.join(" ")
795                )
796            }
797            MissingCommand::XFlowey {
798                description,
799                xflowey_args: args,
800            } => anyhow::bail!(
801                "Failed to find {}. Run `cargo xflowey {}` to create it.",
802                description,
803                args.join(" ")
804            ),
805            MissingCommand::Restore { description } => {
806                anyhow::bail!(
807                    "Failed to find {}. Run `cargo xflowey restore-packages`.",
808                    description
809                )
810            }
811            MissingCommand::Custom { description, cmd } => {
812                anyhow::bail!(
813                    "Failed to find {}. Run `{}` to create it.",
814                    description,
815                    cmd
816                )
817            }
818        }
819    }
820}