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 anyhow::Context;
9use fs_err::PathExt;
10use petri_artifacts_common::tags::MachineArch;
11use petri_artifacts_core::ArtifactSource;
12use petri_artifacts_core::AsArtifactHandle;
13use petri_artifacts_core::ErasedArtifactHandle;
14use std::env::consts::EXE_EXTENSION;
15use std::path::Path;
16use std::path::PathBuf;
17use vmm_test_images::CONTAINER;
18use vmm_test_images::KnownTestArtifacts;
19use vmm_test_images::STORAGE_ACCOUNT;
20
21/// Returns the Cargo build profile directory name for cross-compiled
22/// artifacts (e.g., pipette).
23///
24/// Infers the profile from the currently running binary's path (looking
25/// for a `release` component in the executable path). Defaults to `"debug"`.
26// DEVNOTE: `pub` in order to re-use in perf_tests and other crates.
27pub fn cargo_build_profile() -> &'static str {
28    static PROFILE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
29    PROFILE.get_or_init(|| {
30        if let Ok(exe) = std::env::current_exe() {
31            if exe.components().any(|c| c.as_os_str() == "release") {
32                return "release".to_string();
33            }
34        }
35        "debug".to_string()
36    })
37}
38
39/// An implementation of [`petri_artifacts_core::ResolveTestArtifact`]
40/// that resolves artifacts to various "known paths" within the context of
41/// the OpenVMM repository.
42pub struct OpenvmmKnownPathsTestArtifactResolver<'a>(&'a str);
43
44impl<'a> OpenvmmKnownPathsTestArtifactResolver<'a> {
45    /// Creates a new resolver for a test with the given name.
46    pub fn new(test_name: &'a str) -> Self {
47        Self(test_name)
48    }
49}
50
51impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifactResolver<'_> {
52    #[rustfmt::skip]
53    fn resolve(&self, id: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
54        use petri_artifacts_common::artifacts as common;
55        use petri_artifacts_vmm_test::artifacts::*;
56        use petri_artifacts_vmm_test::tags::IsHostedOnHvliteAzureBlobStore;
57
58        match id {
59            _ if id == common::PIPETTE_WINDOWS_X64 => pipette_path(MachineArch::X86_64, PipetteFlavor::Windows),
60            _ if id == common::PIPETTE_LINUX_X64 => pipette_path(MachineArch::X86_64, PipetteFlavor::Linux),
61            _ if id == common::PIPETTE_WINDOWS_AARCH64 => pipette_path(MachineArch::Aarch64, PipetteFlavor::Windows),
62            _ if id == common::PIPETTE_LINUX_AARCH64 => pipette_path(MachineArch::Aarch64, PipetteFlavor::Linux),
63
64            _ if id == common::TEST_LOG_DIRECTORY => test_log_directory_path(self.0),
65
66            _ if id == OPENVMM_NATIVE => openvmm_native_executable_path(),
67            #[cfg(target_os = "linux")]
68            _ if id == OPENVMM_VHOST_NATIVE => openvmm_vhost_native_executable_path(),
69
70            _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_X64 => linux_direct_x64_test_kernel_path(),
71            _ if id == loadable::LINUX_DIRECT_TEST_BZIMAGE_X64 => linux_direct_x64_test_bzimage_path(),
72            _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64 => linux_direct_arm_image_path(),
73            _ if id == loadable::LINUX_DIRECT_TEST_INITRD_X64 => linux_direct_test_initrd_path(MachineArch::X86_64),
74            _ if id == loadable::LINUX_DIRECT_TEST_INITRD_AARCH64 => linux_direct_test_initrd_path(MachineArch::Aarch64),
75
76            _ if id == petritools::PETRITOOLS_EROFS_X64 => petritools_erofs_path(MachineArch::X86_64),
77            _ if id == petritools::PETRITOOLS_EROFS_AARCH64 => petritools_erofs_path(MachineArch::Aarch64),
78
79            _ if id == loadable::PCAT_FIRMWARE_X64 => pcat_firmware_path(),
80            _ if id == loadable::SVGA_FIRMWARE_X64 => svga_firmware_path(),
81            _ if id == loadable::UEFI_FIRMWARE_X64 => uefi_firmware_path(MachineArch::X86_64),
82            _ if id == loadable::UEFI_FIRMWARE_AARCH64 => uefi_firmware_path(MachineArch::Aarch64),
83
84            _ if id == openhcl_igvm::LATEST_STANDARD_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Standard),
85            _ if id == openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel),
86            _ if id == openhcl_igvm::LATEST_CVM_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Cvm),
87            _ if id == openhcl_igvm::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::LinuxDirect),
88            _ if id == openhcl_igvm::LATEST_STANDARD_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::Standard),
89            _ if id == openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel),
90
91            _ if id == openhcl_igvm::LATEST_RELEASE_STANDARD_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Release2511, OpenhclFlavor::Standard),
92            _ if id == openhcl_igvm::LATEST_RELEASE_LINUX_DIRECT_X64 => openhcl_bin_path(MachineArch::X86_64, OpenhclVersion::Release2511, OpenhclFlavor::LinuxDirect),
93            _ if id == openhcl_igvm::LATEST_RELEASE_STANDARD_AARCH64 => openhcl_bin_path(MachineArch::Aarch64, OpenhclVersion::Release2511, OpenhclFlavor::Standard),
94
95            _ if id == openhcl_igvm::um_bin::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_extras_path(OpenhclVersion::Latest,OpenhclFlavor::LinuxDirect,OpenhclExtras::UmBin),
96            _ if id == openhcl_igvm::um_dbg::LATEST_LINUX_DIRECT_TEST_X64 => openhcl_extras_path(OpenhclVersion::Latest,OpenhclFlavor::LinuxDirect,OpenhclExtras::UmDbg),
97
98            _ if id == test_vhd::GUEST_TEST_UEFI_X64 => guest_test_uefi_disk_path(MachineArch::X86_64),
99            _ if id == test_vhd::GUEST_TEST_UEFI_AARCH64 => guest_test_uefi_disk_path(MachineArch::Aarch64),
100
101            _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64_PREPPED => {
102                let base_filename = test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64::FILENAME;
103                let prepped_filename = base_filename.replace(".vhd", "-prepped.vhd");
104                let images_dir = std::env::var("VMM_TEST_IMAGES");
105                let full_path = Path::new(images_dir.as_deref().unwrap_or("images"));
106                get_path(
107                    full_path,
108                    prepped_filename,
109                    MissingCommand::Run {
110                        description: "prepped test image",
111                        package: "prep_steps",
112                    },
113                )
114            }
115
116            _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2022_X64_NO_VMBUS_PREPPED => {
117                let base_filename = test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2022_X64::FILENAME;
118                let prepped_filename = base_filename.replace(".vhd", "-no-vmbus-prepped.vhd");
119                let images_dir = std::env::var("VMM_TEST_IMAGES");
120                let full_path = Path::new(images_dir.as_deref().unwrap_or("images"));
121                get_path(
122                    full_path,
123                    prepped_filename,
124                    MissingCommand::Custom {
125                        description: "no-vmbus prepped test image",
126                        cmd: "cargo run -p prep_steps -- no-vmbus",
127                    },
128                )
129            }
130
131            _ if id == tmks::TMK_VMM_NATIVE => tmk_vmm_native_executable_path(),
132            _ if id == tmks::TMK_VMM_LINUX_X64 => tmk_vmm_linux_path(MachineArch::X86_64),
133            _ if id == tmks::TMK_VMM_LINUX_AARCH64 =>
134                env_path_or(OPENVMM_CCA_TMK_VMM_ENV_VAR, || {
135                    tmk_vmm_linux_path(MachineArch::Aarch64)
136                }),
137            _ if id == tmks::TMK_VMM_LINUX_X64_MUSL => tmk_vmm_paravisor_path(MachineArch::X86_64),
138            _ if id == tmks::TMK_VMM_LINUX_AARCH64_MUSL => tmk_vmm_paravisor_path(MachineArch::Aarch64),
139            _ if id == tmks::SIMPLE_TMK_X64 => simple_tmk_path(MachineArch::X86_64),
140            _ if id == tmks::SIMPLE_TMK_AARCH64 =>
141                env_path_or(OPENVMM_CCA_SIMPLE_TMK_ENV_VAR, || {
142                    simple_tmk_path(MachineArch::Aarch64)
143                }),
144
145            _ if id == cca::SHRINKWRAP => cca_shrinkwrap_path(),
146            _ if id == cca::VENV => cca_venv_path(),
147            _ if id == cca::ROOTFS => cca_package_path("rootfs.ext2", "CCA emulation rootfs"),
148            _ if id == cca::E2FSCK => cca_buildroot_host_sbin_path("e2fsck", "CCA buildroot host e2fsck"),
149            _ if id == cca::RESIZE2FS => cca_buildroot_host_sbin_path("resize2fs", "CCA buildroot host resize2fs"),
150            _ if id == cca::GUEST_DISK => cca_package_path("guest-disk.img", "CCA guest disk"),
151            _ if id == cca::PLANE0_LINUX_IMAGE => cca_plane0_linux_image_path(),
152            _ if id == cca::KVMTOOL_EFI => cca_package_path("KVMTOOL_EFI.fd", "CCA kvmtool EFI firmware"),
153            _ if id == cca::LKVM => cca_package_path("lkvm", "CCA lkvm"),
154
155            _ if id == vmgstool::VMGSTOOL_NATIVE => vmgstool_native_executable_path(),
156            _ if id == vmgstool::VMGSTOOL_DEV_NATIVE => vmgstool_dev_native_executable_path(),
157
158            _ if id == guest_tools::TPM_GUEST_TESTS_WINDOWS_X64 => {
159                tpm_guest_tests_windows_path(MachineArch::X86_64)
160            }
161            _ if id == guest_tools::TPM_GUEST_TESTS_LINUX_X64 => {
162                tpm_guest_tests_linux_path(MachineArch::X86_64)
163            }
164
165            _ if id == virtio_win::VIRTIO_WIN_DRIVERS => {
166                virtio_win_path()
167            }
168
169            _ if id == host_tools::TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64 => {
170                test_igvm_agent_rpc_server_windows_path(MachineArch::X86_64)
171            }
172
173            // Blob-hosted artifacts: resolved via blob_artifact_info.
174            _ if let Some(artifact) = KnownTestArtifacts::from_handle(id) => {
175                get_test_artifact_path(artifact)
176            }
177
178            _ => anyhow::bail!("no support for given artifact type"),
179        }
180    }
181
182    fn resolve_source(&self, id: ErasedArtifactHandle) -> anyhow::Result<ArtifactSource> {
183        // Try local resolution first.
184        let local_err = match self.resolve(id) {
185            Ok(path) => return Ok(ArtifactSource::Local(path)),
186            Err(e) => e,
187        };
188
189        // Fall back to remote URL for artifacts hosted on Azure Blob Storage,
190        // but only for formats the blob disk backend supports (fixed VHD1 and flat).
191        if let Some(artifact) = KnownTestArtifacts::from_handle(id) {
192            if artifact.supports_blob_disk() {
193                let url = format!(
194                    "https://{STORAGE_ACCOUNT}.blob.core.windows.net/{CONTAINER}/{}",
195                    artifact.filename()
196                );
197                return Ok(ArtifactSource::Remote { url });
198            }
199        }
200
201        // No local path and no remote URL available — return the original error.
202        Err(local_err)
203    }
204}
205
206/// Returns the bundle-relative file name for the given artifact.
207///
208/// This is the `file_name` argument that [`get_path`] would use when
209/// resolving this artifact. When creating a self-contained bundle for
210/// deployment, place the artifact at this relative path within the
211/// bundle directory, then set `VMM_TESTS_CONTENT_DIR` to the bundle
212/// directory at runtime.
213///
214/// Returns `None` for artifacts that don't have a fixed bundle name
215/// (e.g., log directories).
216pub fn resolve_bundle_name(id: ErasedArtifactHandle) -> Option<&'static str> {
217    use petri_artifacts_common::artifacts as common;
218    use petri_artifacts_vmm_test::artifacts::*;
219
220    match id {
221        _ if id == common::PIPETTE_LINUX_X64 => Some("pipette"),
222        _ if id == common::PIPETTE_LINUX_AARCH64 => Some("pipette"),
223        _ if id == common::PIPETTE_WINDOWS_X64 => Some("pipette.exe"),
224        _ if id == common::PIPETTE_WINDOWS_AARCH64 => Some("pipette.exe"),
225        _ if id == OPENVMM_NATIVE => Some(if cfg!(windows) {
226            "openvmm.exe"
227        } else {
228            "openvmm"
229        }),
230        _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_X64 => Some("x64/vmlinux"),
231        _ if id == loadable::LINUX_DIRECT_TEST_BZIMAGE_X64 => Some("x64/bzImage"),
232        _ if id == loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64 => Some("aarch64/Image"),
233        _ if id == loadable::LINUX_DIRECT_TEST_INITRD_X64 => Some("x64/initrd"),
234        _ if id == loadable::LINUX_DIRECT_TEST_INITRD_AARCH64 => Some("aarch64/initrd"),
235        _ if id == petritools::PETRITOOLS_EROFS_X64 => Some("x64/petritools.erofs"),
236        _ if id == petritools::PETRITOOLS_EROFS_AARCH64 => Some("aarch64/petritools.erofs"),
237        _ if id == loadable::UEFI_FIRMWARE_X64 => {
238            Some("hyperv.uefi.mscoreuefi.x64.RELEASE/MsvmX64/RELEASE_VS2022/FV/MSVM.fd")
239        }
240        _ if id == loadable::UEFI_FIRMWARE_AARCH64 => {
241            Some("hyperv.uefi.mscoreuefi.AARCH64.RELEASE/MsvmAARCH64/RELEASE_CLANGPDB/FV/MSVM.fd")
242        }
243        _ => {
244            // For test VHDs, the bundle name is the artifact filename from
245            // IsHostedOnHvliteAzureBlobStore. Use resolve_test_vhd_bundle_name
246            // for those.
247            None
248        }
249    }
250}
251
252#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
253enum PipetteFlavor {
254    Windows,
255    Linux,
256}
257
258#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
259enum OpenhclVersion {
260    Latest,
261    Release2511,
262}
263
264#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
265enum OpenhclFlavor {
266    Standard,
267    StandardDevKernel,
268    Cvm,
269    LinuxDirect,
270}
271
272#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
273enum OpenhclExtras {
274    UmBin,
275    UmDbg,
276}
277
278/// The architecture specific fragment of the name of the directory used by rust when referring to specific targets.
279fn target_arch_path(arch: MachineArch) -> &'static str {
280    match arch {
281        MachineArch::X86_64 => "x86_64",
282        MachineArch::Aarch64 => "aarch64",
283    }
284}
285
286fn windows_msvc_target(arch: MachineArch) -> &'static str {
287    match arch {
288        MachineArch::X86_64 => "x86_64-pc-windows-msvc",
289        MachineArch::Aarch64 => "aarch64-pc-windows-msvc",
290    }
291}
292
293fn get_test_artifact_path(artifact: KnownTestArtifacts) -> Result<PathBuf, anyhow::Error> {
294    let images_dir = std::env::var("VMM_TEST_IMAGES");
295    let full_path = Path::new(images_dir.as_deref().unwrap_or("images"));
296
297    get_path(
298        full_path,
299        artifact.filename(),
300        MissingCommand::Xtask {
301            xtask_args: &[
302                "guest-test",
303                "download-image",
304                "--artifacts",
305                artifact.name(),
306            ],
307            description: "test artifact",
308        },
309    )
310}
311
312/// Path to the output location of our guest-test image for UEFI.
313fn guest_test_uefi_disk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
314    // `guest_test_uefi` is always at `{arch}-unknown-uefi/debug`
315    get_path(
316        format!("target/{}-unknown-uefi/debug", target_arch_path(arch)),
317        "guest_test_uefi.img",
318        MissingCommand::Xtask {
319            xtask_args: &[
320                "guest-test",
321                "uefi",
322                &format!(
323                    "--boot{}",
324                    match arch {
325                        MachineArch::X86_64 => "x64",
326                        MachineArch::Aarch64 => "aa64",
327                    }
328                ),
329            ],
330            description: "guest_test_uefi image",
331        },
332    )
333}
334
335/// Path to the output location of the pipette executable.
336fn pipette_path(arch: MachineArch, os_flavor: PipetteFlavor) -> anyhow::Result<PathBuf> {
337    // Always use (statically-built) musl on Linux to avoid needing libc
338    // compatibility.
339    let (target_suffixes, binary) = match os_flavor {
340        PipetteFlavor::Windows => (vec!["pc-windows-msvc", "pc-windows-gnu"], "pipette.exe"),
341        PipetteFlavor::Linux => (vec!["unknown-linux-musl"], "pipette"),
342    };
343    for (index, target_suffix) in target_suffixes.iter().enumerate() {
344        let target = format!("{}-{}", target_arch_path(arch), target_suffix);
345        match get_path(
346            format!("target/{target}/{}", cargo_build_profile()),
347            binary,
348            MissingCommand::Build {
349                package: "pipette",
350                target: Some(&target),
351            },
352        ) {
353            Ok(path) => return Ok(path),
354            Err(err) => {
355                if index < target_suffixes.len() - 1 {
356                    continue;
357                } else {
358                    anyhow::bail!(
359                        "None of the suffixes {:?} had `pipette` built, {err:?}",
360                        target_suffixes
361                    );
362                }
363            }
364        }
365    }
366
367    unreachable!()
368}
369
370/// Path to the output location of the openvmm executable.
371fn openvmm_native_executable_path() -> anyhow::Result<PathBuf> {
372    get_output_executable_path("openvmm")
373}
374
375/// Path to the output location of the openvmm_vhost executable.
376#[cfg(target_os = "linux")]
377fn openvmm_vhost_native_executable_path() -> anyhow::Result<PathBuf> {
378    get_output_executable_path("openvmm_vhost")
379}
380
381/// Path to the output location of the tmk_vmm executable.
382fn tmk_vmm_native_executable_path() -> anyhow::Result<PathBuf> {
383    get_output_executable_path("tmk_vmm")
384}
385
386/// Path to the output location of the vmgstool executable.
387fn vmgstool_native_executable_path() -> anyhow::Result<PathBuf> {
388    get_output_executable_path("vmgstool")
389}
390
391/// Path to the output location of the vmgstool-dev executable.
392fn vmgstool_dev_native_executable_path() -> anyhow::Result<PathBuf> {
393    get_path(
394        "target/debug",
395        Path::new("vmgstool-dev").with_extension(EXE_EXTENSION),
396        MissingCommand::Custom {
397            description: "vmgstool-dev (Cargo build output must be renamed to match)",
398            cmd: "cargo build -p vmgstool --features encryption,test_helpers",
399        },
400    )
401}
402
403/// Path to the output location of the tpm_guest_tests executable.
404fn tpm_guest_tests_windows_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
405    let target = windows_msvc_target(arch);
406    get_path(
407        format!("target/{target}/debug"),
408        "tpm_guest_tests.exe",
409        MissingCommand::Build {
410            package: "tpm_guest_tests",
411            target: Some(target),
412        },
413    )
414}
415
416fn tpm_guest_tests_linux_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
417    let target = match arch {
418        MachineArch::X86_64 => "x86_64-unknown-linux-gnu",
419        MachineArch::Aarch64 => "aarch64-unknown-linux-gnu",
420    };
421
422    get_path(
423        format!("target/{target}/debug"),
424        "tpm_guest_tests",
425        MissingCommand::Build {
426            package: "tpm_guest_tests",
427            target: Some(target),
428        },
429    )
430}
431
432/// Path to the output location of the test_igvm_agent_rpc_server executable.
433fn test_igvm_agent_rpc_server_windows_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
434    let target = windows_msvc_target(arch);
435    get_path(
436        format!("target/{target}/debug"),
437        "test_igvm_agent_rpc_server.exe",
438        MissingCommand::Build {
439            package: "test_igvm_agent_rpc_server",
440            target: Some(target),
441        },
442    )
443}
444
445/// Path to the extracted virtio-win driver package from openvmm-deps.
446fn virtio_win_path() -> anyhow::Result<PathBuf> {
447    get_path(
448        ".packages",
449        "virtio-win",
450        MissingCommand::Restore {
451            description: "virtio-win drivers",
452        },
453    )
454}
455
456const OPENVMM_CCA_TEST_ROOT_ENV_VAR: &str = "OPENVMM_CCA_TEST_ROOT";
457const OPENVMM_CCA_TMK_VMM_ENV_VAR: &str = "OPENVMM_CCA_TMK_VMM";
458const OPENVMM_CCA_SIMPLE_TMK_ENV_VAR: &str = "OPENVMM_CCA_SIMPLE_TMK";
459
460fn cca_missing_command(description: &'static str) -> MissingCommand<'static> {
461    MissingCommand::XFlowey {
462        description,
463        xflowey_args: &["cca-tests", "--install-emu"],
464    }
465}
466
467fn env_path_or(
468    name: &str,
469    fallback: impl FnOnce() -> anyhow::Result<PathBuf>,
470) -> anyhow::Result<PathBuf> {
471    std::env::var_os(name)
472        .map(PathBuf::from)
473        .map(Ok)
474        .unwrap_or_else(fallback)
475}
476
477fn cca_test_root() -> PathBuf {
478    std::env::var_os(OPENVMM_CCA_TEST_ROOT_ENV_VAR)
479        .map(PathBuf::from)
480        .unwrap_or_else(|| PathBuf::from("target/cca-test"))
481}
482
483fn cca_home_dir() -> anyhow::Result<PathBuf> {
484    std::env::var_os("HOME")
485        .map(PathBuf::from)
486        .ok_or_else(|| anyhow::anyhow!("HOME is not set"))
487}
488
489fn cca_shrinkwrap_path() -> anyhow::Result<PathBuf> {
490    get_path(
491        cca_test_root().join("shrinkwrap"),
492        "shrinkwrap/shrinkwrap",
493        cca_missing_command("CCA shrinkwrap executable"),
494    )
495}
496
497fn cca_venv_path() -> anyhow::Result<PathBuf> {
498    get_path(
499        cca_test_root().join("shrinkwrap"),
500        "venv",
501        cca_missing_command("CCA shrinkwrap virtual environment"),
502    )
503}
504
505fn cca_plane0_linux_image_path() -> anyhow::Result<PathBuf> {
506    get_path(
507        cca_test_root().join("plane0-linux/arch/arm64/boot"),
508        "Image",
509        cca_missing_command("CCA Plane0 Linux image"),
510    )
511}
512
513fn cca_package_path(file_name: &'static str, description: &'static str) -> anyhow::Result<PathBuf> {
514    get_path(
515        cca_home_dir()?.join(".shrinkwrap/package/cca-3world"),
516        file_name,
517        cca_missing_command(description),
518    )
519}
520
521fn cca_buildroot_host_sbin_path(
522    file_name: &'static str,
523    description: &'static str,
524) -> anyhow::Result<PathBuf> {
525    get_path(
526        cca_home_dir()?.join(".shrinkwrap/build/build/cca-3world/buildroot/host/sbin"),
527        file_name,
528        cca_missing_command(description),
529    )
530}
531
532/// Path to the output location of the GNU/Linux tmk_vmm executable.
533fn tmk_vmm_linux_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
534    let target = match arch {
535        MachineArch::X86_64 => "x86_64-unknown-linux-gnu",
536        MachineArch::Aarch64 => "aarch64-unknown-linux-gnu",
537    };
538
539    if let Some(path) = try_get_path(format!("target/{target}/debug"), "tmk_vmm")? {
540        return Ok(path);
541    }
542
543    if let Some(path) =
544        flowey_built_executable_path(format!("target/tmk_vmm/{target}/debug/deps"), "tmk_vmm")?
545    {
546        return Ok(path);
547    }
548
549    get_path(
550        format!("target/{target}/debug"),
551        "tmk_vmm",
552        MissingCommand::Build {
553            package: "tmk_vmm",
554            target: Some(target),
555        },
556    )
557}
558
559/// Path to the output location of the musl/Linux tmk_vmm executable.
560fn tmk_vmm_paravisor_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
561    let target = match arch {
562        MachineArch::X86_64 => "x86_64-unknown-linux-musl",
563        MachineArch::Aarch64 => "aarch64-unknown-linux-musl",
564    };
565    get_path(
566        format!("target/{target}/debug"),
567        "tmk_vmm",
568        MissingCommand::Build {
569            package: "tmk_vmm",
570            target: Some(target),
571        },
572    )
573}
574
575/// Path to the output location of the simple_tmk executable.
576fn simple_tmk_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
577    let arch_str = match arch {
578        MachineArch::X86_64 => "x86_64",
579        MachineArch::Aarch64 => "aarch64",
580    };
581    let target = match arch {
582        MachineArch::X86_64 => "x86_64-unknown-none",
583        MachineArch::Aarch64 => "aarch64-minimal_rt-none",
584    };
585
586    if let Some(path) = try_get_path(format!("target/{target}/debug"), "simple_tmk")? {
587        return Ok(path);
588    }
589
590    if let Some(path) = flowey_built_executable_path(
591        format!("target/simple_tmk/{target}/debug/deps"),
592        "simple_tmk",
593    )? {
594        return Ok(path);
595    }
596
597    get_path(
598        format!("target/{target}/debug"),
599        "simple_tmk",
600        MissingCommand::Custom {
601            description: "simple_tmk",
602            cmd: &format!(
603                "RUSTC_BOOTSTRAP=1 cargo build -p simple_tmk --config openhcl/minimal_rt/{arch_str}-config.toml"
604            ),
605        },
606    )
607}
608
609/// Path to our packaged linux direct test kernel.
610fn linux_direct_x64_test_kernel_path() -> anyhow::Result<PathBuf> {
611    use petri_artifacts_vmm_test::artifacts::loadable;
612    get_path(
613        ".packages/underhill-deps-private",
614        resolve_bundle_name(loadable::LINUX_DIRECT_TEST_KERNEL_X64.erase()).unwrap(),
615        MissingCommand::Restore {
616            description: "linux direct test kernel",
617        },
618    )
619}
620
621/// Path to our packaged linux direct test bzImage.
622fn linux_direct_x64_test_bzimage_path() -> anyhow::Result<PathBuf> {
623    use petri_artifacts_vmm_test::artifacts::loadable;
624    get_path(
625        ".packages/underhill-deps-private",
626        resolve_bundle_name(loadable::LINUX_DIRECT_TEST_BZIMAGE_X64.erase()).unwrap(),
627        MissingCommand::Restore {
628            description: "linux direct test bzImage",
629        },
630    )
631}
632
633/// Path to our packaged linux direct test initrd.
634fn linux_direct_test_initrd_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
635    use petri_artifacts_vmm_test::artifacts::loadable;
636    let id = match arch {
637        MachineArch::X86_64 => loadable::LINUX_DIRECT_TEST_INITRD_X64.erase(),
638        MachineArch::Aarch64 => loadable::LINUX_DIRECT_TEST_INITRD_AARCH64.erase(),
639    };
640    get_path(
641        ".packages/underhill-deps-private",
642        resolve_bundle_name(id).unwrap(),
643        MissingCommand::Restore {
644            description: "linux direct test initrd",
645        },
646    )
647}
648
649/// Path to our packaged petritools erofs image.
650fn petritools_erofs_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
651    use petri_artifacts_vmm_test::artifacts::petritools;
652    let id = match arch {
653        MachineArch::X86_64 => petritools::PETRITOOLS_EROFS_X64.erase(),
654        MachineArch::Aarch64 => petritools::PETRITOOLS_EROFS_AARCH64.erase(),
655    };
656    get_path(
657        ".packages/underhill-deps-private",
658        resolve_bundle_name(id).unwrap(),
659        MissingCommand::Restore {
660            description: "petritools erofs image",
661        },
662    )
663}
664
665/// Path to our packaged linux direct test kernel.
666fn linux_direct_arm_image_path() -> anyhow::Result<PathBuf> {
667    use petri_artifacts_vmm_test::artifacts::loadable;
668    get_path(
669        ".packages/underhill-deps-private",
670        resolve_bundle_name(loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64.erase()).unwrap(),
671        MissingCommand::Restore {
672            description: "linux direct test kernel",
673        },
674    )
675}
676
677/// Path to our packaged PCAT firmware.
678fn pcat_firmware_path() -> anyhow::Result<PathBuf> {
679    get_path(
680        ".packages",
681        "Microsoft.Windows.VmFirmware.Pcat.amd64fre/content/vmfirmwarepcat.dll",
682        MissingCommand::Restore {
683            description: "PCAT firmware binary",
684        },
685    )
686}
687
688/// Path to our packaged SVGA firmware.
689fn svga_firmware_path() -> anyhow::Result<PathBuf> {
690    get_path(
691        ".packages",
692        "Microsoft.Windows.VmEmulatedDevices.amd64fre/content/VmEmulatedDevices.dll",
693        MissingCommand::Restore {
694            description: "SVGA firmware binary",
695        },
696    )
697}
698
699/// Path to our packaged UEFI firmware image.
700fn uefi_firmware_path(arch: MachineArch) -> anyhow::Result<PathBuf> {
701    use petri_artifacts_vmm_test::artifacts::loadable;
702    let id = match arch {
703        MachineArch::X86_64 => loadable::UEFI_FIRMWARE_X64.erase(),
704        MachineArch::Aarch64 => loadable::UEFI_FIRMWARE_AARCH64.erase(),
705    };
706    get_path(
707        ".packages",
708        resolve_bundle_name(id).unwrap(),
709        MissingCommand::Restore {
710            description: "UEFI firmware binary",
711        },
712    )
713}
714
715/// Path to the output location of the requested OpenHCL package.
716fn openhcl_bin_path(
717    arch: MachineArch,
718    version: OpenhclVersion,
719    flavor: OpenhclFlavor,
720) -> anyhow::Result<PathBuf> {
721    let (path, name, cmd) = match (arch, version, flavor) {
722        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Standard) => (
723            "flowey-out/artifacts/build-igvm/debug/x64",
724            "openhcl-x64.bin",
725            MissingCommand::XFlowey {
726                description: "OpenHCL IGVM file",
727                xflowey_args: &["build-igvm", "x64"],
728            },
729        ),
730        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel) => (
731            "flowey-out/artifacts/build-igvm/debug/x64-devkern",
732            "openhcl-x64-devkern.bin",
733            MissingCommand::XFlowey {
734                description: "OpenHCL IGVM file",
735                xflowey_args: &["build-igvm", "x64-devkern"],
736            },
737        ),
738        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::Cvm) => (
739            "flowey-out/artifacts/build-igvm/debug/x64-cvm",
740            "openhcl-x64-cvm.bin",
741            MissingCommand::XFlowey {
742                description: "OpenHCL IGVM file",
743                xflowey_args: &["build-igvm", "x64-cvm"],
744            },
745        ),
746        (MachineArch::X86_64, OpenhclVersion::Latest, OpenhclFlavor::LinuxDirect) => (
747            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
748            "openhcl-x64-test-linux-direct.bin",
749            MissingCommand::XFlowey {
750                description: "OpenHCL IGVM file",
751                xflowey_args: &["build-igvm", "x64-test-linux-direct"],
752            },
753        ),
754        (MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::Standard) => (
755            "flowey-out/artifacts/build-igvm/debug/aarch64",
756            "openhcl-aarch64.bin",
757            MissingCommand::XFlowey {
758                description: "OpenHCL IGVM file",
759                xflowey_args: &["build-igvm", "aarch64"],
760            },
761        ),
762        (MachineArch::Aarch64, OpenhclVersion::Latest, OpenhclFlavor::StandardDevKernel) => (
763            "flowey-out/artifacts/build-igvm/debug/aarch64-devkern",
764            "openhcl-aarch64-devkern.bin",
765            MissingCommand::XFlowey {
766                description: "OpenHCL IGVM file",
767                xflowey_args: &["build-igvm", "aarch64-devkern"],
768            },
769        ),
770        (MachineArch::X86_64, OpenhclVersion::Release2511, OpenhclFlavor::LinuxDirect) => (
771            "flowey-out/artifacts/last-release-igvm-files",
772            "release-2511-x64-direct-openhcl.bin",
773            MissingCommand::XFlowey {
774                description: "Previous OpenHCL release IGVM file",
775                xflowey_args: &["restore-packages"],
776            },
777        ),
778        (MachineArch::X86_64, OpenhclVersion::Release2511, OpenhclFlavor::Standard) => (
779            "flowey-out/artifacts/last-release-igvm-files",
780            "release-2511-x64-openhcl.bin",
781            MissingCommand::XFlowey {
782                description: "Previous OpenHCL release IGVM file",
783                xflowey_args: &["restore-packages"],
784            },
785        ),
786        (MachineArch::Aarch64, OpenhclVersion::Release2511, OpenhclFlavor::Standard) => (
787            "flowey-out/artifacts/last-release-igvm-files",
788            "release-2511-aarch64-openhcl.bin",
789            MissingCommand::XFlowey {
790                description: "Previous OpenHCL release IGVM file",
791                xflowey_args: &["restore-packages"],
792            },
793        ),
794        _ => anyhow::bail!("no openhcl bin with given arch, version, and flavor"),
795    };
796
797    get_path(path, name, cmd)
798}
799
800/// Path to the specified build artifact for the requested OpenHCL package.
801fn openhcl_extras_path(
802    version: OpenhclVersion,
803    flavor: OpenhclFlavor,
804    item: OpenhclExtras,
805) -> anyhow::Result<PathBuf> {
806    if !matches!(version, OpenhclVersion::Latest) || !matches!(flavor, OpenhclFlavor::LinuxDirect) {
807        anyhow::bail!("Debug symbol path currently only available for LATEST_LINUX_DIRECT_TEST")
808    }
809
810    let (path, name) = match item {
811        OpenhclExtras::UmBin => (
812            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
813            "openvmm_hcl_msft",
814        ),
815        OpenhclExtras::UmDbg => (
816            "flowey-out/artifacts/build-igvm/debug/x64-test-linux-direct",
817            "openvmm_hcl_msft.dbg",
818        ),
819    };
820
821    get_path(
822        path,
823        name,
824        MissingCommand::XFlowey {
825            description: "OpenHCL IGVM file",
826            xflowey_args: &["build-igvm", "x64-test-linux-direct"],
827        },
828    )
829}
830
831/// Path to the per-test test output directory.
832fn test_log_directory_path(test_name: &str) -> anyhow::Result<PathBuf> {
833    let root = if let Some(path) = std::env::var_os("TEST_OUTPUT_PATH") {
834        PathBuf::from(path)
835    } else {
836        get_repo_root()?.join("vmm_test_results")
837    };
838    // Use a per-test subdirectory, replacing `::` with `__` to avoid issues
839    // with filesystems that don't support `::` in filenames.
840    let path = root.join(test_name.replace("::", "__"));
841    fs_err::create_dir_all(&path)?;
842    Ok(path)
843}
844
845const VMM_TESTS_DIR_ENV_VAR: &str = "VMM_TESTS_CONTENT_DIR";
846
847/// Gets a path to the root of the repo.
848pub fn get_repo_root() -> anyhow::Result<PathBuf> {
849    Ok(Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."))
850}
851
852fn try_get_path(
853    search_path: impl AsRef<Path>,
854    file_name: impl AsRef<Path>,
855) -> anyhow::Result<Option<PathBuf>> {
856    let search_path = search_path.as_ref();
857    let file_name = file_name.as_ref();
858    if file_name.is_absolute() {
859        anyhow::bail!("{} should be a relative path", file_name.display());
860    }
861
862    if let Ok(env_dir) = std::env::var(VMM_TESTS_DIR_ENV_VAR) {
863        let full_path = Path::new(&env_dir).join(file_name);
864        if full_path.try_exists()? {
865            return Ok(Some(full_path));
866        }
867    }
868
869    let file_path = if search_path.is_absolute() {
870        search_path.to_owned()
871    } else {
872        get_repo_root()?.join(search_path)
873    };
874
875    let full_path = file_path.join(file_name);
876    Ok(full_path.try_exists()?.then_some(full_path))
877}
878
879fn flowey_built_executable_path(
880    search_path: impl AsRef<Path>,
881    binary_prefix: &str,
882) -> anyhow::Result<Option<PathBuf>> {
883    let search_path = search_path.as_ref();
884    let dir = if search_path.is_absolute() {
885        search_path.to_owned()
886    } else {
887        get_repo_root()?.join(search_path)
888    };
889
890    if !dir.is_dir() {
891        return Ok(None);
892    }
893
894    let mut candidates = Vec::new();
895    for entry in std::fs::read_dir(&dir)? {
896        let entry = entry?;
897        let path = entry.path();
898        if !path.is_file() {
899            continue;
900        }
901
902        if path.extension().is_some() {
903            continue;
904        }
905
906        let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
907            continue;
908        };
909
910        if file_name == binary_prefix || file_name.starts_with(&format!("{binary_prefix}-")) {
911            let modified = entry.metadata()?.modified().ok();
912            candidates.push((modified, path));
913        }
914    }
915
916    candidates.sort_by_key(|(modified, _)| *modified);
917    Ok(candidates.pop().map(|(_, path)| path))
918}
919
920/// Attempts to find the given file, first checking for it relative to the test
921/// content directory, then falling back to the provided search path.
922///
923/// Note that the file name can be a multi-segment path (e.g. `foo/bar.txt`) so
924/// that it must be in subdirectory of the test content directory. This is useful
925/// when multiple files with the same name are needed in different contexts.
926///
927/// If the search path is relative it is treated as relative to the repo root.
928/// If it is absolute it is used unchanged.
929///
930/// If the file cannot be found then the provided command will be returned as an
931/// easily printable error.
932// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
933pub fn get_path(
934    search_path: impl AsRef<Path>,
935    file_name: impl AsRef<Path>,
936    missing_cmd: MissingCommand<'_>,
937) -> anyhow::Result<PathBuf> {
938    let search_path = search_path.as_ref();
939    let file_name = file_name.as_ref();
940    if file_name.is_absolute() {
941        anyhow::bail!("{} should be a relative path", file_name.display());
942    }
943
944    if let Ok(env_dir) = std::env::var(VMM_TESTS_DIR_ENV_VAR) {
945        let full_path = Path::new(&env_dir).join(file_name);
946        if full_path.fs_err_try_exists()? {
947            return Ok(full_path);
948        }
949    }
950
951    let file_path = if search_path.is_absolute() {
952        search_path.to_owned()
953    } else {
954        get_repo_root()?.join(search_path)
955    };
956
957    let full_path = file_path.join(file_name);
958    if !full_path.fs_err_try_exists()? {
959        missing_cmd
960            .to_error()
961            .with_context(|| format!("failed to find {}", full_path.display()))?;
962    }
963
964    Ok(full_path)
965}
966
967/// Attempts to find the path to a rust executable built by Cargo, checking
968/// the test content directory if the environment variable is set.
969// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
970pub fn get_output_executable_path(name: &str) -> anyhow::Result<PathBuf> {
971    let mut path: PathBuf = std::env::current_exe()?;
972    // Sometimes we end up inside deps instead of the output dir, but if we
973    // are we can just go up a level.
974    if path.parent().and_then(|x| x.file_name()).unwrap() == "deps" {
975        path.pop();
976    }
977
978    get_path(
979        path.parent().unwrap(),
980        Path::new(name).with_extension(EXE_EXTENSION),
981        MissingCommand::Build {
982            package: name,
983            target: None,
984        },
985    )
986}
987
988/// A description of a command that can be run to create a missing file.
989// DEVNOTE: `pub` in order to re-use logic in closed-source known_paths resolver
990#[derive(Copy, Clone)]
991#[expect(missing_docs)] // Self-describing field names.
992pub enum MissingCommand<'a> {
993    /// A `cargo build` invocation.
994    Build {
995        package: &'a str,
996        target: Option<&'a str>,
997    },
998    /// A `cargo run` invocation.
999    Run {
1000        description: &'a str,
1001        package: &'a str,
1002    },
1003    /// A `cargo xtask` invocation.
1004    Xtask {
1005        description: &'a str,
1006        xtask_args: &'a [&'a str],
1007    },
1008    /// A `cargo xflowey` invocation.
1009    XFlowey {
1010        description: &'a str,
1011        xflowey_args: &'a [&'a str],
1012    },
1013    /// A `xflowey restore-packages` invocation.
1014    Restore { description: &'a str },
1015    /// A custom command.
1016    Custom { description: &'a str, cmd: &'a str },
1017}
1018
1019impl MissingCommand<'_> {
1020    fn to_error(self) -> anyhow::Result<()> {
1021        match self {
1022            MissingCommand::Build { package, target } => anyhow::bail!(
1023                "Failed to find {package} binary. Run `cargo build {target_args}-p {package}` to build it.",
1024                target_args =
1025                    target.map_or(String::new(), |target| format!("--target {} ", target)),
1026            ),
1027            MissingCommand::Run {
1028                description,
1029                package,
1030            } => anyhow::bail!(
1031                "Failed to find {}. Run `cargo run -p {}` to create it.",
1032                description,
1033                package
1034            ),
1035            MissingCommand::Xtask {
1036                description,
1037                xtask_args: args,
1038            } => {
1039                anyhow::bail!(
1040                    "Failed to find {}. Run `cargo xtask {}` to create it.",
1041                    description,
1042                    args.join(" ")
1043                )
1044            }
1045            MissingCommand::XFlowey {
1046                description,
1047                xflowey_args: args,
1048            } => anyhow::bail!(
1049                "Failed to find {}. Run `cargo xflowey {}` to create it.",
1050                description,
1051                args.join(" ")
1052            ),
1053            MissingCommand::Restore { description } => {
1054                anyhow::bail!(
1055                    "Failed to find {}. Run `cargo xflowey restore-packages`.",
1056                    description
1057                )
1058            }
1059            MissingCommand::Custom { description, cmd } => {
1060                anyhow::bail!(
1061                    "Failed to find {}. Run `{}` to create it.",
1062                    description,
1063                    cmd
1064                )
1065            }
1066        }
1067    }
1068}