Skip to main content

flowey_hvlite/pipelines/
vmm_tests_run.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Pipeline to discover artifacts and run VMM tests in a single command.
5//!
6//! This pipeline:
7//! 1. Discovers required artifacts for the specified test filter (at pipeline
8//!    construction time)
9//! 2. Builds the necessary dependencies
10//! 3. Runs the tests
11
12use anyhow::Context as _;
13use flowey::node::prelude::ReadVar;
14use flowey::pipeline::prelude::*;
15use flowey_lib_hvlite::_jobs::local_build_and_run_nextest_vmm_tests::BuildSelections;
16use flowey_lib_hvlite::_jobs::local_build_and_run_nextest_vmm_tests::VmmTestSelections;
17use flowey_lib_hvlite::common::CommonTriple;
18use flowey_lib_hvlite::install_vmm_tests_deps::VmmTestsDepSelections;
19use flowey_lib_hvlite::install_vmm_tests_deps::VmmTestsDepSelectionsWindows;
20use petri_artifacts_core::ArtifactId;
21use petri_artifacts_core::ArtifactListOutput;
22use std::collections::BTreeMap;
23use std::collections::BTreeSet;
24use std::io::Write as _;
25use std::path::Path;
26use std::path::PathBuf;
27use std::process::Command;
28use std::process::Stdio;
29use vmm_test_images::KnownTestArtifacts;
30
31/// Build and run VMM tests with automatic artifact discovery
32#[derive(clap::Args)]
33pub struct VmmTestsRunCli {
34    /// Specify what target to build the VMM tests for
35    ///
36    /// If not specified, defaults to the current host target.
37    #[clap(long)]
38    target: Option<VmmTestTargetCli>,
39
40    /// Directory for the output artifacts.
41    ///
42    /// If not specified, defaults to `target/vmm_tests`.
43    /// WSL-to-Windows runs still require explicitly overriding this to a
44    /// Windows-accessible output directory.
45    #[clap(long)]
46    dir: Option<PathBuf>,
47
48    /// Test filter (nextest filter expression)
49    ///
50    /// Examples:
51    ///   - `test(alpine)` - run tests with "alpine" in the name
52    ///   - `test(/^boot_/)` - run tests starting with "boot_"
53    ///   - `all()` - run all tests
54    #[clap(long, default_value = "all()")]
55    filter: String,
56
57    /// pass `--verbose` to cargo
58    #[clap(long)]
59    verbose: bool,
60    /// Automatically install any missing required dependencies.
61    #[clap(long)]
62    install_missing_deps: bool,
63
64    /// Release build instead of debug build
65    #[clap(long)]
66    release: bool,
67
68    /// Build only, do not run
69    #[clap(long)]
70    build_only: bool,
71    /// Copy extras to output dir (symbols, etc)
72    #[clap(long)]
73    copy_extras: bool,
74
75    /// Skip the interactive VHD download prompt
76    #[clap(long)]
77    skip_vhd_prompt: bool,
78
79    /// Download all disk images upfront instead of streaming on demand.
80    ///
81    /// By default, VHD/ISO disk images are streamed on demand via HTTP
82    /// and cached locally, avoiding large upfront downloads. Use this
83    /// flag to force all images to be downloaded before tests run.
84    #[clap(long)]
85    no_lazy_fetch: bool,
86
87    /// Optional: custom kernel modules
88    #[clap(long)]
89    custom_kernel_modules: Option<PathBuf>,
90    /// Optional: custom kernel image
91    #[clap(long)]
92    custom_kernel: Option<PathBuf>,
93    /// Optional: custom UEFI firmware (MSVM.fd) to use instead of the
94    /// downloaded release. Path to a locally-built MSVM.fd file.
95    #[clap(long)]
96    custom_uefi_firmware: Option<PathBuf>,
97
98    /// use the nextest CI profile rather than the default one
99    #[clap(long)]
100    ci_profile: bool,
101
102    /// Don't reuse prepped vhds, even if they already exist.
103    /// Use when making changes to prep_steps
104    #[clap(long)]
105    no_reuse_prepped_vhds: bool,
106
107    /// Disable secure AVIC support for SNP. This adds the
108    /// `disable_secure_avic` cargo feature and sets `secure_avic` to
109    /// `disabled` in the IGVM manifest.
110    #[clap(long)]
111    pub disable_secure_avic: bool,
112}
113
114struct CargoNextestListRequest<'a> {
115    repo_root: &'a Path,
116    target: &'a str,
117    filter: &'a str,
118    release: bool,
119    include_ignored: bool,
120}
121
122struct RustSuite {
123    binary_path: PathBuf,
124    testcases: Vec<String>,
125}
126
127/// Result of resolving artifact requirements to build/download selections
128#[derive(Default, Debug)]
129struct ResolvedArtifactSelections {
130    /// What to build
131    build: BuildSelections,
132    /// What to download
133    downloads: BTreeSet<KnownTestArtifacts>,
134    /// Whether any tests need release IGVM files from GitHub
135    needs_release_igvm: bool,
136    /// Whether any of the tests require Hyper-V
137    needs_hyperv: bool,
138    /// Whether any of the tests require hardware isolation
139    needs_hardware_isolation: bool,
140}
141
142impl IntoPipeline for VmmTestsRunCli {
143    fn into_pipeline(self, backend_hint: PipelineBackendHint) -> anyhow::Result<Pipeline> {
144        if !matches!(backend_hint, PipelineBackendHint::Local) {
145            anyhow::bail!("vmm-tests-run is for local use only")
146        }
147
148        let Self {
149            target,
150            dir,
151            filter,
152            verbose,
153            install_missing_deps,
154            release,
155            build_only,
156            copy_extras,
157            skip_vhd_prompt,
158            no_lazy_fetch,
159            custom_kernel_modules,
160            custom_kernel,
161            custom_uefi_firmware,
162            ci_profile,
163            no_reuse_prepped_vhds,
164            disable_secure_avic,
165        } = self;
166
167        let target = resolve_target(target, backend_hint)?;
168        let target_os = target.as_triple().operating_system;
169        let target_architecture = target.common_arch()?;
170        let target_str = target.as_triple().to_string();
171
172        let repo_root = crate::repo_root();
173
174        // Validate output directory for WSL
175        validate_output_dir(dir.as_deref(), target_os)?;
176        let test_content_dir = dir.unwrap_or_else(|| repo_root.join("target").join("vmm_tests"));
177        std::fs::create_dir_all(&test_content_dir).context("failed to create output directory")?;
178
179        // Run artifact discovery inline at pipeline construction time since
180        // flowey doesn't support conditional requests yet
181        log::info!(
182            "Discovering artifacts for filter: {} (target: {})",
183            filter,
184            target
185        );
186
187        // Determine which tests match the filter
188        let suites = run_cargo_nextest_list(CargoNextestListRequest {
189            repo_root: &repo_root,
190            target: &target_str,
191            filter: &filter,
192            release,
193            // When using build-only mode, we need to enumerate tests that could be
194            // run on any system so that we build all necessary dependencies. By default
195            // petri marks incompatible tests as ignored.
196            include_ignored: build_only,
197        })?;
198
199        if suites.is_empty() {
200            anyhow::bail!("No tests found for the given filter");
201        }
202
203        // Query for the required artifacts
204        let mut artifacts = Vec::new();
205        for suite in suites.values() {
206            artifacts.append(&mut query_test_binary_artifacts(suite)?);
207        }
208
209        // Resolve to build selections
210        let mut resolved = ResolvedArtifactSelections::default();
211        for artifact in artifacts {
212            resolved.resolve_artifact(&artifact)?;
213        }
214
215        // Determine whether we need hyper-v and/or hardware isolation
216        resolved.needs_hyperv = suites
217            .values()
218            .any(|s| s.testcases.iter().any(|name| name.contains("hyperv")));
219        resolved.needs_hardware_isolation = suites.values().any(|s| {
220            s.testcases
221                .iter()
222                .any(|name| name.contains("snp") || name.contains("tdx"))
223        });
224
225        // Determine lazy fetch mode.
226        //
227        // By default, VHD/ISO downloads are skipped and disk images are
228        // streamed on demand via HTTP (with local SQLite caching). This
229        // avoids multi-GB upfront downloads for dev-inner-loop scenarios.
230        //
231        // Lazy fetch is disabled for all downloads when the user passes
232        // --no-lazy-fetch and for any downloads that are used by a selected
233        // Hyper-V test.
234        //
235        // When both Hyper-V and non-Hyper-V tests are selected, only the
236        // artifacts required by Hyper-V tests are downloaded upfront; the
237        // rest are lazy-fetched.
238        if no_lazy_fetch {
239            log::info!("Lazy fetch disabled");
240        } else {
241            let mut hyperv_tests: usize = 0;
242            let mut hyperv_artifacts = Vec::new();
243            for (_, suite) in suites.iter() {
244                let hyperv_testcases: Vec<_> = suite
245                    .testcases
246                    .iter()
247                    .filter(|name| name.contains("hyperv"))
248                    .cloned()
249                    .collect();
250
251                if !hyperv_testcases.is_empty() {
252                    hyperv_tests += hyperv_testcases.len();
253                    hyperv_artifacts.append(&mut query_test_binary_artifacts(&RustSuite {
254                        binary_path: suite.binary_path.clone(),
255                        testcases: hyperv_testcases,
256                    })?);
257                }
258            }
259
260            resolved.downloads.retain(|a| !a.supports_blob_disk());
261
262            if hyperv_tests == 0 {
263                log::info!("Lazy fetch enabled: disk images will be streamed on demand via HTTP");
264            } else {
265                log::info!(
266                    "Downloading disk images required by {} Hyper-V tests",
267                    hyperv_tests
268                );
269            }
270
271            // Re-add only the downloads needed for hyper-v. Other selections should
272            // remain the same since resolve_artifact can only add selections
273            for artifact in hyperv_artifacts {
274                resolved.resolve_artifact(&artifact)?;
275            }
276        }
277
278        log::info!("Resolved selections: {:?}", resolved);
279
280        let openvmm_repo = flowey_lib_common::git_checkout::RepoSource::ExistingClone(
281            ReadVar::from_static(repo_root),
282        );
283
284        let mut pipeline = Pipeline::new();
285
286        let mut job = pipeline.new_job(
287            FlowPlatform::host(backend_hint),
288            FlowArch::host(backend_hint),
289            "build all dependencies and run vmm tests",
290        );
291
292        job = job.dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init);
293
294        // Override kernel with local paths if both kernel and modules are specified
295        if let (Some(kernel_path), Some(modules_path)) =
296            (custom_kernel.clone(), custom_kernel_modules.clone())
297        {
298            job =
299                job.dep_on(
300                    move |_| flowey_lib_hvlite::_jobs::cfg_versions::Request::LocalKernel {
301                        arch: target_architecture,
302                        kernel: ReadVar::from_static(kernel_path),
303                        modules: ReadVar::from_static(modules_path),
304                    },
305                );
306        }
307
308        // Override UEFI firmware with a local MSVM.fd path
309        if let Some(fw_path) = custom_uefi_firmware {
310            job = job.dep_on(move |_| {
311                flowey_lib_hvlite::_jobs::cfg_versions::Request::LocalUefi(
312                    target_architecture,
313                    ReadVar::from_static(fw_path),
314                )
315            });
316        }
317
318        job = job
319            .dep_on(
320                |_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params {
321                    hvlite_repo_source: openvmm_repo.clone(),
322                },
323            )
324            .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params {
325                local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams {
326                    interactive: true,
327                    auto_install: install_missing_deps,
328                    ignore_rust_version: true,
329                }),
330                verbose: ReadVar::from_static(verbose),
331                locked: false,
332                deny_warnings: false,
333                no_incremental: false,
334            })
335            .dep_on(|ctx| {
336                flowey_lib_hvlite::_jobs::local_build_and_run_nextest_vmm_tests::Params {
337                    target,
338                    test_content_dir,
339                    selections: selections_from_resolved(filter, resolved, target_os),
340                    release,
341                    build_only,
342                    copy_extras,
343                    custom_kernel_modules,
344                    custom_kernel,
345                    skip_vhd_prompt,
346                    nextest_profile: if ci_profile {
347                        flowey_lib_hvlite::run_cargo_nextest_run::NextestProfile::Ci
348                    } else {
349                        flowey_lib_hvlite::run_cargo_nextest_run::NextestProfile::Default
350                    },
351                    reuse_prepped_vhds: !no_reuse_prepped_vhds,
352                    disable_secure_avic,
353                    done: ctx.new_done_handle(),
354                }
355            });
356
357        job.finish();
358
359        Ok(pipeline)
360    }
361}
362
363/// Get test binaries and associated matching tests for a given nextest filter.
364// TODO: this function should really be a flowey node without automatic
365// dependency installation, but that would require conditional requests.
366fn run_cargo_nextest_list<'a>(
367    req: CargoNextestListRequest<'a>,
368) -> anyhow::Result<BTreeMap<String, RustSuite>> {
369    let CargoNextestListRequest {
370        repo_root,
371        target,
372        filter,
373        release,
374        include_ignored,
375    } = req;
376
377    // Check that cargo-nextest is available
378    let nextest_check = Command::new("cargo")
379        .args(["nextest", "--version"])
380        .stdout(Stdio::null())
381        .stderr(Stdio::null())
382        .status();
383    match nextest_check {
384        Ok(status) if status.success() => {}
385        _ => anyhow::bail!(
386            "cargo-nextest not found. Run 'cargo install --locked cargo-nextest' first."
387        ),
388    }
389
390    // Step 1: Use nextest to resolve the filter expression to test names and
391    // get the binary path
392    let mut cmd = Command::new("cargo");
393    cmd.stderr(Stdio::inherit());
394    cmd.current_dir(repo_root).args([
395        "nextest",
396        "list",
397        "-p",
398        "vmm_tests",
399        "--target",
400        target,
401        "--filter-expr",
402        filter,
403        "--message-format",
404        "json",
405    ]);
406    if release {
407        cmd.arg("--release");
408    }
409    if include_ignored {
410        cmd.args(["--run-ignored", "all"]);
411    }
412    let nextest_output = cmd.output().context("failed to run cargo nextest list")?;
413    anyhow::ensure!(nextest_output.status.success(), "cargo nextest list failed",);
414    let nextest_stdout = String::from_utf8(nextest_output.stdout)
415        .map_err(|e| anyhow::anyhow!("nextest output is not valid UTF-8: {}", e))?;
416
417    parse_nextest_output(&nextest_stdout)
418}
419
420/// Parse `cargo nextest list --message-format json` output to extract test
421/// names and binary path.
422fn parse_nextest_output(stdout: &str) -> anyhow::Result<BTreeMap<String, RustSuite>> {
423    let json: serde_json::Value = serde_json::from_str(stdout)
424        .map_err(|e| anyhow::anyhow!("failed to parse nextest JSON output: {}", e))?;
425
426    let mut suites = BTreeMap::new();
427
428    for (name, suite) in json
429        .get("rust-suites")
430        .and_then(|s| s.as_object())
431        .context("no rust-suites object")?
432    {
433        let binary_path = PathBuf::from(
434            suite
435                .get("binary-path")
436                .and_then(|v| v.as_str())
437                .context("no binary-path str")?,
438        );
439
440        let testcases: Vec<_> = suite
441            .get("testcases")
442            .and_then(|t| t.as_object())
443            .context("no testcases object")?
444            .iter()
445            .filter(|(_, test_info)| {
446                test_info
447                    .get("filter-match")
448                    .and_then(|fm| fm.get("status"))
449                    .and_then(|s| s.as_str())
450                    .is_some_and(|s| s == "matches")
451            })
452            .map(|(test_name, _)| test_name.to_owned())
453            .collect();
454
455        if !testcases.is_empty() {
456            suites.insert(
457                name.to_owned(),
458                RustSuite {
459                    binary_path,
460                    testcases,
461                },
462            );
463        }
464    }
465
466    Ok(suites)
467}
468
469/// Runs the test binary with `--list-required-artifacts --tests-from-stdin`
470/// and returns all the required and optional artifacts for all test defined
471/// in the RustSuite.
472fn query_test_binary_artifacts(suite: &RustSuite) -> anyhow::Result<Vec<String>> {
473    log::info!("Using test binary: {}", suite.binary_path.display());
474    log::info!("Querying artifacts for {} tests", suite.testcases.len());
475    let stdin_data = suite
476        .testcases
477        .iter()
478        .map(|n| format!("{n}\n"))
479        .collect::<String>();
480    let mut child = Command::new(&suite.binary_path)
481        .args(["--list-required-artifacts", "--tests-from-stdin"])
482        .stdin(Stdio::piped())
483        .stdout(Stdio::piped())
484        .stderr(Stdio::piped())
485        .spawn()
486        .context("failed to spawn test binary")?;
487
488    child
489        .stdin
490        .take()
491        .expect("stdin was piped")
492        .write_all(stdin_data.as_bytes())
493        .context("failed to write test names to stdin")?;
494
495    let artifact_output = child
496        .wait_with_output()
497        .context("failed to wait for test binary")?;
498    anyhow::ensure!(
499        artifact_output.status.success(),
500        "test binary failed: {}",
501        String::from_utf8_lossy(&artifact_output.stderr)
502    );
503    let artifact_stdout = String::from_utf8(artifact_output.stdout)
504        .map_err(|e| anyhow::anyhow!("test output is not valid UTF-8: {}", e))?;
505
506    let ArtifactListOutput {
507        mut required,
508        mut optional,
509    } = serde_json::from_str(&artifact_stdout)
510        .map_err(|e| anyhow::anyhow!("failed to parse test output JSON: {}", e))?;
511
512    let mut artifacts = Vec::new();
513    artifacts.append(&mut required);
514    artifacts.append(&mut optional);
515    Ok(artifacts)
516}
517
518#[derive(clap::ValueEnum, Copy, Clone)]
519enum VmmTestTargetCli {
520    /// Windows Aarch64
521    WindowsAarch64,
522    /// Windows X64
523    WindowsX64,
524    /// Linux X64
525    LinuxX64,
526}
527
528/// Resolve a CLI target option to a CommonTriple, defaulting to the host.
529fn resolve_target(
530    target: Option<VmmTestTargetCli>,
531    backend_hint: PipelineBackendHint,
532) -> anyhow::Result<CommonTriple> {
533    let target = if let Some(t) = target {
534        t
535    } else {
536        match (
537            FlowArch::host(backend_hint),
538            FlowPlatform::host(backend_hint),
539        ) {
540            (FlowArch::Aarch64, FlowPlatform::Windows) => VmmTestTargetCli::WindowsAarch64,
541            (FlowArch::X86_64, FlowPlatform::Windows) => VmmTestTargetCli::WindowsX64,
542            (FlowArch::X86_64, FlowPlatform::Linux(_)) => VmmTestTargetCli::LinuxX64,
543            _ => anyhow::bail!("unsupported host"),
544        }
545    };
546
547    Ok(match target {
548        VmmTestTargetCli::WindowsAarch64 => CommonTriple::AARCH64_WINDOWS_MSVC,
549        VmmTestTargetCli::WindowsX64 => CommonTriple::X86_64_WINDOWS_MSVC,
550        VmmTestTargetCli::LinuxX64 => CommonTriple::X86_64_LINUX_GNU,
551    })
552}
553
554/// Validate the output directory path based on the current platform.
555///
556/// When running under WSL and targeting Windows, the output directory must be a
557/// Windows-accessible path (DrvFs mount like `/mnt/c/...`) because Windows
558/// requires VHDs to reside on a Windows filesystem. On native Windows or Linux
559/// this check is a no-op.
560fn validate_output_dir(
561    dir: Option<&Path>,
562    target_os: target_lexicon::OperatingSystem,
563) -> anyhow::Result<()> {
564    if flowey_cli::running_in_wsl() && matches!(target_os, target_lexicon::OperatingSystem::Windows)
565    {
566        if let Some(dir) = dir {
567            if !flowey_cli::is_wsl_windows_path(dir) {
568                anyhow::bail!(
569                    "When targeting Windows from WSL, --dir must be a path on Windows \
570                        (i.e., on a DrvFs mount like /mnt/c/vmm_tests). \
571                        Got: {}",
572                    dir.display()
573                );
574            }
575        } else {
576            anyhow::bail!(
577                "An output directory on the Windows filesystem \
578                    must be specified when targeting Windows from WSL."
579            )
580        }
581    }
582    Ok(())
583}
584
585/// Resolve `ResolvedArtifactSelections` to `VmmTestSelections`.
586fn selections_from_resolved(
587    filter: String,
588    resolved: ResolvedArtifactSelections,
589    target_os: target_lexicon::OperatingSystem,
590) -> VmmTestSelections {
591    VmmTestSelections {
592        filter,
593        artifacts: resolved.downloads.into_iter().collect(),
594        build: resolved.build.clone(),
595        deps: match target_os {
596            target_lexicon::OperatingSystem::Windows => {
597                VmmTestsDepSelections::Windows(VmmTestsDepSelectionsWindows {
598                    hyperv: resolved.needs_hyperv,
599                    whp: resolved.build.openvmm,
600                    hardware_isolation: resolved.needs_hardware_isolation,
601                })
602            }
603            target_lexicon::OperatingSystem::Linux => VmmTestsDepSelections::Linux,
604            _ => unreachable!(),
605        },
606        needs_release_igvm: resolved.needs_release_igvm,
607    }
608}
609
610impl ResolvedArtifactSelections {
611    /// Resolve a single artifact ID and update selections.
612    fn resolve_artifact(&mut self, id: &str) -> anyhow::Result<()> {
613        match id {
614            // OpenVMM binary
615            petri_artifacts_vmm_test::artifacts::OPENVMM_WIN_X64::GLOBAL_UNIQUE_ID
616            | petri_artifacts_vmm_test::artifacts::OPENVMM_LINUX_X64::GLOBAL_UNIQUE_ID
617            | petri_artifacts_vmm_test::artifacts::OPENVMM_WIN_AARCH64::GLOBAL_UNIQUE_ID
618            | petri_artifacts_vmm_test::artifacts::OPENVMM_LINUX_AARCH64::GLOBAL_UNIQUE_ID
619            | petri_artifacts_vmm_test::artifacts::OPENVMM_MACOS_AARCH64::GLOBAL_UNIQUE_ID => {
620                self.build.openvmm = true;
621            }
622
623            // OpenVMM vhost binary (Linux only)
624            petri_artifacts_vmm_test::artifacts::OPENVMM_VHOST_LINUX_X64::GLOBAL_UNIQUE_ID
625            | petri_artifacts_vmm_test::artifacts::OPENVMM_VHOST_LINUX_AARCH64 ::GLOBAL_UNIQUE_ID => {
626                self.build.openvmm_vhost = true;
627            }
628
629            // OpenHCL IGVM files
630            petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_X64::GLOBAL_UNIQUE_ID
631            | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_AARCH64::GLOBAL_UNIQUE_ID =>
632            {
633                self.build.openhcl_standard = true;
634            }
635            petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_X64::GLOBAL_UNIQUE_ID
636            | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_AARCH64::GLOBAL_UNIQUE_ID => {
637                self.build.openhcl_standard_dev = true;
638            }
639            petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_CVM_X64::GLOBAL_UNIQUE_ID
640             =>
641            {
642                self.build.openhcl_cvm = true;
643            }
644            petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_LINUX_DIRECT_TEST_X64::GLOBAL_UNIQUE_ID =>
645            {
646                self.build.openhcl_linux_direct = true;
647            }
648
649            // Release IGVM files (downloaded, not built)
650            petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_STANDARD_X64::GLOBAL_UNIQUE_ID
651            | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_LINUX_DIRECT_X64::GLOBAL_UNIQUE_ID
652            | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_STANDARD_AARCH64::GLOBAL_UNIQUE_ID =>
653            {
654                // These are downloaded from GitHub releases, not built
655                self.needs_release_igvm = true;
656            }
657
658            // Guest test UEFI
659            petri_artifacts_vmm_test::artifacts::test_vhd::GUEST_TEST_UEFI_X64::GLOBAL_UNIQUE_ID
660            | petri_artifacts_vmm_test::artifacts::test_vhd::GUEST_TEST_UEFI_AARCH64 ::GLOBAL_UNIQUE_ID => {
661                self.build.guest_test_uefi = true;
662            }
663
664            // TMKs
665            petri_artifacts_vmm_test::artifacts::tmks::SIMPLE_TMK_X64::GLOBAL_UNIQUE_ID
666            | petri_artifacts_vmm_test::artifacts::tmks::SIMPLE_TMK_AARCH64 ::GLOBAL_UNIQUE_ID => {
667                self.build.tmks = true;
668            }
669
670            // TMK VMM
671            petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_WIN_X64::GLOBAL_UNIQUE_ID
672            | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_WIN_AARCH64::GLOBAL_UNIQUE_ID => {
673                self.build.tmk_vmm_windows = true;
674            }
675            petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_X64::GLOBAL_UNIQUE_ID
676            | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_AARCH64::GLOBAL_UNIQUE_ID
677            | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_X64_MUSL::GLOBAL_UNIQUE_ID
678            | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_AARCH64_MUSL::GLOBAL_UNIQUE_ID
679            | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_MACOS_AARCH64::GLOBAL_UNIQUE_ID => {
680                self.build.tmk_vmm_linux = true;
681            }
682
683            // VmgsTool
684            petri_artifacts_vmm_test::artifacts::VMGSTOOL_WIN_X64::GLOBAL_UNIQUE_ID
685            | petri_artifacts_vmm_test::artifacts::VMGSTOOL_WIN_AARCH64::GLOBAL_UNIQUE_ID => {
686                self.build.vmgstool = true;
687            }
688            petri_artifacts_vmm_test::artifacts::VMGSTOOL_LINUX_X64::GLOBAL_UNIQUE_ID
689            | petri_artifacts_vmm_test::artifacts::VMGSTOOL_LINUX_AARCH64::GLOBAL_UNIQUE_ID
690            | petri_artifacts_vmm_test::artifacts::VMGSTOOL_MACOS_AARCH64::GLOBAL_UNIQUE_ID => {
691                self.build.vmgstool = true;
692            }
693
694            // TPM guest tests
695            petri_artifacts_vmm_test::artifacts::guest_tools::TPM_GUEST_TESTS_WINDOWS_X64::GLOBAL_UNIQUE_ID => {
696                self.build.tpm_guest_tests_windows = true;
697            }
698            petri_artifacts_vmm_test::artifacts::guest_tools::TPM_GUEST_TESTS_LINUX_X64::GLOBAL_UNIQUE_ID => {
699                self.build.tpm_guest_tests_linux = true;
700            }
701
702            // Host tools
703            petri_artifacts_vmm_test::artifacts::host_tools::TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64::GLOBAL_UNIQUE_ID =>
704            {
705                self.build.test_igvm_agent_rpc_server = true;
706            }
707
708            // Loadable firmware artifacts (these come from deps, not built)
709            petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_KERNEL_X64::GLOBAL_UNIQUE_ID
710            | petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_INITRD_X64::GLOBAL_UNIQUE_ID
711            | petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_BZIMAGE_X64::GLOBAL_UNIQUE_ID
712            | petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64::GLOBAL_UNIQUE_ID
713            | petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_INITRD_AARCH64::GLOBAL_UNIQUE_ID
714            | petri_artifacts_vmm_test::artifacts::loadable::PCAT_FIRMWARE_X64::GLOBAL_UNIQUE_ID
715            | petri_artifacts_vmm_test::artifacts::loadable::SVGA_FIRMWARE_X64::GLOBAL_UNIQUE_ID
716            | petri_artifacts_vmm_test::artifacts::loadable::UEFI_FIRMWARE_X64::GLOBAL_UNIQUE_ID
717            | petri_artifacts_vmm_test::artifacts::loadable::UEFI_FIRMWARE_AARCH64::GLOBAL_UNIQUE_ID => {
718                // These are resolved from OpenVMM deps, always available
719            }
720
721            // Test VHDs
722            petri_artifacts_vmm_test::artifacts::test_vhd::GEN1_WINDOWS_DATA_CENTER_CORE2022_X64::GLOBAL_UNIQUE_ID =>
723            {
724                self.downloads
725                    .insert(KnownTestArtifacts::Gen1WindowsDataCenterCore2022X64Vhd);
726            }
727            petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2022_X64::GLOBAL_UNIQUE_ID =>
728            {
729                self.downloads
730                    .insert(KnownTestArtifacts::Gen2WindowsDataCenterCore2022X64Vhd);
731            }
732            petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64::GLOBAL_UNIQUE_ID =>
733            {
734                self.downloads
735                    .insert(KnownTestArtifacts::Gen2WindowsDataCenterCore2025X64Vhd);
736            }
737            petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64_PREPPED::GLOBAL_UNIQUE_ID =>
738            {
739                self.build.prep_steps = true;
740            }
741            petri_artifacts_vmm_test::artifacts::test_vhd::FREE_BSD_13_2_X64::GLOBAL_UNIQUE_ID => {
742                self.downloads.insert(KnownTestArtifacts::FreeBsd13_2X64Vhd);
743            }
744            petri_artifacts_vmm_test::artifacts::test_vhd::ALPINE_3_23_X64::GLOBAL_UNIQUE_ID => {
745                self.downloads.insert(KnownTestArtifacts::Alpine323X64Vhd);
746            }
747            petri_artifacts_vmm_test::artifacts::test_vhd::ALPINE_3_23_AARCH64::GLOBAL_UNIQUE_ID => {
748                self.downloads
749                    .insert(KnownTestArtifacts::Alpine323Aarch64Vhd);
750            }
751            petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2404_SERVER_X64::GLOBAL_UNIQUE_ID => {
752                self.downloads
753                    .insert(KnownTestArtifacts::Ubuntu2404ServerX64Vhd);
754            }
755            petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2504_SERVER_X64::GLOBAL_UNIQUE_ID => {
756                self.downloads
757                    .insert(KnownTestArtifacts::Ubuntu2504ServerX64Vhd);
758            }
759            petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2404_SERVER_AARCH64::GLOBAL_UNIQUE_ID => {
760                self.downloads
761                    .insert(KnownTestArtifacts::Ubuntu2404ServerAarch64Vhd);
762            }
763            petri_artifacts_vmm_test::artifacts::test_vhd::WINDOWS_11_ENTERPRISE_AARCH64::GLOBAL_UNIQUE_ID => {
764                self.downloads
765                    .insert(KnownTestArtifacts::Windows11EnterpriseAarch64Vhdx);
766            }
767
768            // Test ISOs (downloaded)
769            petri_artifacts_vmm_test::artifacts::test_iso::FREE_BSD_13_2_X64::GLOBAL_UNIQUE_ID => {
770                self.downloads.insert(KnownTestArtifacts::FreeBsd13_2X64Iso);
771            }
772
773            // Test VMGS files
774            petri_artifacts_vmm_test::artifacts::test_vmgs::VMGS_WITH_BOOT_ENTRY::GLOBAL_UNIQUE_ID => {
775                self.downloads.insert(KnownTestArtifacts::VmgsWithBootEntry);
776            }
777            petri_artifacts_vmm_test::artifacts::test_vmgs::VMGS_WITH_16K_TPM::GLOBAL_UNIQUE_ID => {
778                self.downloads.insert(KnownTestArtifacts::VmgsWith16kTpm);
779            }
780
781            // OpenHCL usermode binaries (built as part of IGVM)
782            petri_artifacts_vmm_test::artifacts::openhcl_igvm::um_bin::LATEST_LINUX_DIRECT_TEST_X64::GLOBAL_UNIQUE_ID
783            | petri_artifacts_vmm_test::artifacts::openhcl_igvm::um_dbg::LATEST_LINUX_DIRECT_TEST_X64::GLOBAL_UNIQUE_ID =>
784            {
785                self.build.openhcl_linux_direct = true;
786            }
787
788            // Common artifacts (always available, no build needed)
789            petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY::GLOBAL_UNIQUE_ID => {}
790
791            // Virtio-win drivers (downloaded from openvmm-deps, always available)
792            petri_artifacts_vmm_test::artifacts::virtio_win::VIRTIO_WIN_DRIVERS::GLOBAL_UNIQUE_ID => {}
793
794            // Pipette binaries (from petri_artifacts_common)
795            petri_artifacts_common::artifacts::PIPETTE_LINUX_X64::GLOBAL_UNIQUE_ID
796            | petri_artifacts_common::artifacts::PIPETTE_LINUX_AARCH64::GLOBAL_UNIQUE_ID => {
797                self.build.pipette_linux = true;
798            }
799            petri_artifacts_common::artifacts::PIPETTE_WINDOWS_X64::GLOBAL_UNIQUE_ID
800            | petri_artifacts_common::artifacts::PIPETTE_WINDOWS_AARCH64::GLOBAL_UNIQUE_ID => {
801                self.build.pipette_windows = true;
802            }
803
804            _ => anyhow::bail!("unknown artifact: {id}"),
805        };
806        Ok(())
807    }
808}