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 petri_artifacts_common::artifacts::*;
10use petri_artifacts_core::ArtifactId;
11use petri_artifacts_core::ArtifactSource;
12use petri_artifacts_core::ErasedArtifactHandle;
13use petri_artifacts_vmm_test::artifacts::*;
14use petri_artifacts_vmm_test::vmm_test_image_from_id;
15use std::env::consts::EXE_EXTENSION;
16use std::path::Path;
17use std::path::PathBuf;
18
19/// An implementation of [`petri_artifacts_core::ResolveTestArtifact`]
20/// that resolves artifacts to various "known paths" within the context of
21/// the OpenVMM repository.
22pub struct OpenvmmKnownPathsTestArtifactResolver<'a>(&'a str);
23
24impl<'a> OpenvmmKnownPathsTestArtifactResolver<'a> {
25    /// Creates a new resolver for a test with the given name.
26    pub fn new(test_name: &'a str) -> Self {
27        Self(test_name)
28    }
29}
30
31impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifactResolver<'_> {
32    fn resolve(&self, handle: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
33        match handle.global_unique_id() {
34            TEST_LOG_DIRECTORY::GLOBAL_UNIQUE_ID => test_log_directory_path(self.0),
35
36            test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64_PREPPED::GLOBAL_UNIQUE_ID
37            | test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2022_X64_NO_VMBUS_PREPPED::GLOBAL_UNIQUE_ID => {
38                get_vmm_test_image_path(handle.filename(), handle.global_unique_id())
39            }
40
41            id if let Some(artifact) = vmm_test_image_from_id(id) => {
42                get_vmm_test_image_path(artifact.filename(), artifact.name())
43            }
44
45            _ => resolve_artifact(handle),
46        }
47    }
48
49    fn resolve_source(&self, handle: ErasedArtifactHandle) -> anyhow::Result<ArtifactSource> {
50        // Try local resolution first.
51        let local_err = match self.resolve(handle) {
52            Ok(path) => return Ok(ArtifactSource::Local(path)),
53            Err(e) => e,
54        };
55
56        // Fall back to remote URL for artifacts hosted on Azure Blob Storage,
57        // but only for formats the blob disk backend supports (fixed VHD1 and flat).
58        if let Some(url) = vmm_test_image_from_id(handle.global_unique_id()).and_then(|i| i.url()) {
59            return Ok(ArtifactSource::Remote { url });
60        }
61
62        // No local path and no remote URL available — return the original error.
63        Err(local_err)
64    }
65}
66
67const VMM_TESTS_CONTENT_DIR_ENV_VAR: &str = "VMM_TESTS_CONTENT_DIR";
68const TEST_OUTPUT_PATH_ENV_VAR: &str = "TEST_OUTPUT_PATH";
69const VMM_TEST_IMAGES_ENV_VAR: &str = "VMM_TEST_IMAGES";
70
71/// Get the path to an artifact from its erased artifact handle
72pub fn resolve_artifact(handle: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
73    let test_content_dir_path = test_content_dir_artifact_path(handle.relative_path());
74
75    if test_content_dir_path.is_ok() {
76        return test_content_dir_path;
77    }
78
79    let magic_path = resolve_magic_path_artifact(handle);
80
81    if magic_path.is_ok() {
82        return magic_path;
83    }
84
85    let target = handle.target_triple();
86    let exe_artifact_path = target
87        .as_ref()
88        .context("no associated triple for artifact")
89        .and_then(|t| get_executable_path_artifact(t, None, handle.filename()));
90
91    if exe_artifact_path.is_ok() {
92        return exe_artifact_path;
93    }
94
95    let relative_path = get_executable_path_relative(handle.filename());
96
97    if relative_path.is_ok() {
98        return relative_path;
99    }
100
101    // also look for the other environment target, since it may work as well
102    let fallback_target = handle.target_triple().and_then(|mut target| {
103        matches!(
104            target,
105            target_lexicon::Triple {
106                operating_system: target_lexicon::OperatingSystem::Linux,
107                environment: target_lexicon::Environment::Gnu | target_lexicon::Environment::Musl,
108                ..
109            }
110        )
111        .then(|| {
112            if target.environment == target_lexicon::Environment::Gnu {
113                target.environment = target_lexicon::Environment::Musl;
114            } else {
115                target.environment = target_lexicon::Environment::Gnu;
116            }
117            target
118        })
119    });
120
121    let fallback_test_content_dir_path = fallback_target
122        .as_ref()
123        .context("no fallback target")
124        .and_then(|t| {
125            test_content_dir_artifact_path(PathBuf::from(t.to_string()).join(handle.filename()))
126        });
127
128    if fallback_test_content_dir_path.is_ok() {
129        return fallback_test_content_dir_path;
130    }
131
132    let fallback_exe_artifact_path = fallback_target
133        .as_ref()
134        .context("no associated triple for artifact")
135        .and_then(|t| get_executable_path_artifact(t, None, handle.filename()));
136
137    if fallback_exe_artifact_path.is_ok() {
138        return fallback_exe_artifact_path;
139    }
140
141    Err(anyhow::anyhow!(
142        "unable to locate {}:\n\t{}\n\t{}\n\t{}\n\t{}\n\t{}\n\t{}",
143        handle.global_unique_id(),
144        test_content_dir_path.unwrap_err(),
145        magic_path.unwrap_err(),
146        exe_artifact_path.unwrap_err(),
147        relative_path.unwrap_err(),
148        fallback_test_content_dir_path.unwrap_err(),
149        fallback_exe_artifact_path.unwrap_err(),
150    ))
151}
152
153fn test_content_dir_artifact_path(relative_path: impl AsRef<Path>) -> anyhow::Result<PathBuf> {
154    let test_content_dir =
155        std::env::var(VMM_TESTS_CONTENT_DIR_ENV_VAR).context("test content dir env var not set")?;
156    let path = PathBuf::from(test_content_dir).join(relative_path.as_ref());
157    if !path.exists() {
158        anyhow::bail!("{} not found", path.display())
159    }
160    Ok(path)
161}
162
163/// Path to the per-test test output directory.
164fn test_log_directory_path(test_name: &str) -> anyhow::Result<PathBuf> {
165    let root = std::env::var_os(TEST_OUTPUT_PATH_ENV_VAR)
166        .map_or_else(|| get_repo_root().join("vmm_test_results"), PathBuf::from);
167    // Use a per-test subdirectory, replacing `::` with `__` to avoid issues
168    // with filesystems that don't support `::` in filenames.
169    let path = root.join(test_name.replace("::", "__"));
170    fs_err::create_dir_all(&path)?;
171    Ok(path)
172}
173
174/// Gets a path to the root of the repo.
175pub fn get_repo_root() -> PathBuf {
176    Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
177}
178
179/// Returns the Cargo build profile directory name for cross-compiled
180/// artifacts (e.g., pipette).
181///
182/// Infers the profile from the currently running binary's path (looking
183/// for a `release` component in the executable path). Defaults to `"debug"`.
184// DEVNOTE: `pub` in order to re-use in perf_tests and other crates.
185pub fn cargo_build_profile() -> &'static str {
186    static PROFILE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
187    PROFILE.get_or_init(|| {
188        if let Ok(exe) = std::env::current_exe() {
189            if exe.components().any(|c| c.as_os_str() == "release") {
190                return "release".to_string();
191            }
192        }
193        "debug".to_string()
194    })
195}
196
197/// Attempts to find the path to a rust executable built by Cargo using the path
198/// to the current executable to find the base path.
199pub fn get_executable_path_relative(name: &str) -> anyhow::Result<PathBuf> {
200    let mut current_exe = std::env::current_exe().context("unable to get current exe")?;
201    // Sometimes we end up inside deps instead of the output dir, but if we
202    // are we can just go up a level.
203    if current_exe.parent().and_then(|x| x.file_name()).unwrap() == "deps" {
204        current_exe.pop();
205    }
206
207    let exe_path = current_exe
208        .parent()
209        .context("current exe has no parent")?
210        .join(Path::new(name).with_extension(EXE_EXTENSION));
211    if !exe_path.exists() {
212        anyhow::bail!("{} not found", exe_path.display());
213    }
214    Ok(exe_path)
215}
216
217/// Attempts to find the path to a rust executable built by Cargo using a path
218/// constructed from the repo root and the default target directory. The build
219/// profile is inferred from the current exe path if not specified.
220pub fn get_executable_path_artifact(
221    target: &target_lexicon::Triple,
222    build_profile: Option<&str>,
223    filename: &str,
224) -> anyhow::Result<PathBuf> {
225    let exe_path = get_repo_root()
226        .join("target")
227        .join(target.to_string())
228        .join(build_profile.unwrap_or_else(|| cargo_build_profile()))
229        .join(filename);
230    if !exe_path.exists() {
231        anyhow::bail!("{} not found", exe_path.display());
232    }
233    Ok(exe_path)
234}
235
236fn get_vmm_test_image_path(filename: &str, name: &str) -> Result<PathBuf, anyhow::Error> {
237    let test_images_dir =
238        std::env::var(VMM_TEST_IMAGES_ENV_VAR).context("test images dir env var not set")?;
239    let path = PathBuf::from(test_images_dir).join(filename);
240    if !path.exists() {
241        anyhow::bail!("missing {} at {}", name, path.display())
242    }
243    Ok(path)
244}
245
246// TODO: refactor flowey/openvmm to have a common source of truth for these
247// magic paths. VMM tests run via flowey no longer use them.
248fn resolve_magic_path_artifact(handle: ErasedArtifactHandle) -> anyhow::Result<PathBuf> {
249    let package_dir = match handle.global_unique_id() {
250        loadable::LINUX_DIRECT_TEST_KERNEL_X64::GLOBAL_UNIQUE_ID
251        | loadable::LINUX_DIRECT_TEST_BZIMAGE_X64::GLOBAL_UNIQUE_ID
252        | loadable::LINUX_DIRECT_TEST_INITRD_X64::GLOBAL_UNIQUE_ID
253        | petritools::PETRITOOLS_EROFS_X64::GLOBAL_UNIQUE_ID => "underhill-deps-private/x64",
254        loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64::GLOBAL_UNIQUE_ID
255        | loadable::LINUX_DIRECT_TEST_INITRD_AARCH64::GLOBAL_UNIQUE_ID
256        | petritools::PETRITOOLS_EROFS_AARCH64::GLOBAL_UNIQUE_ID => {
257            "underhill-deps-private/aarch64"
258        }
259        loadable::UEFI_FIRMWARE_X64::GLOBAL_UNIQUE_ID => {
260            "hyperv.uefi.mscoreuefi.x64.RELEASE/MsvmX64/RELEASE_VS2022/FV"
261        }
262        loadable::UEFI_FIRMWARE_AARCH64::GLOBAL_UNIQUE_ID => {
263            "hyperv.uefi.mscoreuefi.AARCH64.RELEASE/MsvmAARCH64/RELEASE_CLANGPDB/FV"
264        }
265        loadable::PCAT_FIRMWARE_X64::GLOBAL_UNIQUE_ID => {
266            "Microsoft.Windows.VmFirmware.Pcat.amd64fre/content"
267        }
268        loadable::SVGA_FIRMWARE_X64::GLOBAL_UNIQUE_ID => {
269            "Microsoft.Windows.VmEmulatedDevices.amd64fre/content"
270        }
271        virtio_win::VIRTIO_WINDOWS_DRIVERS::GLOBAL_UNIQUE_ID => "",
272
273        _ => anyhow::bail!("not a magic path artifact"),
274    };
275
276    let magic_path = get_repo_root()
277        .join(".packages")
278        .join(package_dir)
279        .join(handle.filename());
280    if !magic_path.exists() {
281        anyhow::bail!("{} not found", magic_path.display());
282    }
283    Ok(magic_path)
284}