Skip to main content

flowey_lib_hvlite/
build_openhcl_igvm_from_recipe.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Build an OpenHCL IGVM file using a particular known-good "recipe", which
5//! encodes the precise features / build parameters used by each constituent
6//! component.
7//!
8//! By having a clearly enumerated list of recipes, it is possible for multiple
9//! pipelines / flows to depend on _precisely_ the same IGVM file, without
10//! having to duplicate the non-trivial OpenHCL IGVM build chain.
11
12use crate::build_openhcl_boot::OpenhclBootOutput;
13use crate::build_openhcl_initrd::OpenhclInitrdExtraParams;
14use crate::build_openvmm_hcl::MaxTraceLevel;
15use crate::build_openvmm_hcl::OpenvmmHclBuildProfile;
16use crate::build_openvmm_hcl::OpenvmmHclFeature;
17use crate::build_openvmm_hcl::OpenvmmHclOutput;
18use crate::build_sidecar::SidecarOutput;
19use crate::common::CommonArch;
20use crate::common::CommonPlatform;
21use crate::common::CommonTriple;
22use crate::resolve_openhcl_kernel_package::OpenhclKernelPackageKind;
23use crate::run_cargo_build::BuildProfile;
24use crate::run_igvmfilegen::IgvmOutput;
25use flowey::node::prelude::*;
26use igvmfilegen_config::ResourceType;
27use std::collections::BTreeMap;
28use std::collections::BTreeSet;
29
30/// OpenHCL IGVM output
31#[derive(Serialize, Deserialize)]
32#[serde(untagged)]
33pub enum OpenhclIgvmOutput {
34    LocalOnlyCustom {
35        #[serde(rename = "openhcl-custom.bin")]
36        igvm_bin: PathBuf,
37        #[serde(flatten)]
38        endorsements: Option<OpenhclIgvmEndorsements>,
39    },
40    X64 {
41        #[serde(rename = "openhcl-x64.bin")]
42        igvm_bin: PathBuf,
43    },
44    X64Devkern {
45        #[serde(rename = "openhcl-x64-devkern.bin")]
46        igvm_bin: PathBuf,
47    },
48    X64TestLinuxDirect {
49        #[serde(rename = "openhcl-x64-test-linux-direct.bin")]
50        igvm_bin: PathBuf,
51    },
52    X64TestSivm {
53        #[serde(rename = "openhcl-x64-test-sivm.bin")]
54        igvm_bin: PathBuf,
55    },
56    X64TestLinuxDirectDevkern {
57        #[serde(rename = "openhcl-x64-test-linux-direct-devkern.bin")]
58        igvm_bin: PathBuf,
59    },
60    X64Cvm {
61        #[serde(rename = "openhcl-x64-cvm.bin")]
62        igvm_bin: PathBuf,
63        #[serde(flatten)]
64        endorsements: Option<OpenhclIgvmEndorsements>,
65    },
66    X64CvmDevkern {
67        #[serde(rename = "openhcl-x64-cvm-devkern.bin")]
68        igvm_bin: PathBuf,
69        #[serde(flatten)]
70        endorsements: Option<OpenhclIgvmEndorsements>,
71    },
72    Aarch64 {
73        #[serde(rename = "openhcl-aarch64.bin")]
74        igvm_bin: PathBuf,
75    },
76    Aarch64Devkern {
77        #[serde(rename = "openhcl-aarch64-devkern.bin")]
78        igvm_bin: PathBuf,
79    },
80}
81
82#[derive(Serialize, Deserialize, Clone)]
83#[serde(untagged)]
84pub enum OpenhclIgvmEndorsements {
85    X64 {
86        #[serde(rename = "openhcl-tdx.json")]
87        #[serde(skip_serializing_if = "Option::is_none")]
88        igvm_tdx_json: Option<PathBuf>,
89        #[serde(rename = "openhcl-snp.json")]
90        #[serde(skip_serializing_if = "Option::is_none")]
91        igvm_snp_json: Option<PathBuf>,
92        #[serde(rename = "openhcl-vbs.json")]
93        #[serde(skip_serializing_if = "Option::is_none")]
94        igvm_vbs_json: Option<PathBuf>,
95        /// The unsigned SNP ID block signing payload emitted by `manifest`.
96        /// This travels with the endorsements (rather than the debug-symbol
97        /// extras) because it is a release/signing input consumed alongside the
98        /// `.json` identity documents. SNP-only, so absent for other platforms.
99        #[serde(rename = "openhcl-snp.idblock")]
100        #[serde(skip_serializing_if = "Option::is_none")]
101        igvm_snp_idblock: Option<PathBuf>,
102        #[serde(rename = "openhcl-tdx.cbor")]
103        #[serde(skip_serializing_if = "Option::is_none")]
104        igvm_tdx_corim: Option<PathBuf>,
105        #[serde(rename = "openhcl-snp.cbor")]
106        #[serde(skip_serializing_if = "Option::is_none")]
107        igvm_snp_corim: Option<PathBuf>,
108        #[serde(rename = "openhcl-vbs.cbor")]
109        #[serde(skip_serializing_if = "Option::is_none")]
110        igvm_vbs_corim: Option<PathBuf>,
111    },
112}
113
114impl OpenhclIgvmEndorsements {
115    fn is_complete(&self) -> bool {
116        match self {
117            OpenhclIgvmEndorsements::X64 {
118                igvm_tdx_json,
119                igvm_snp_json,
120                igvm_vbs_json,
121                igvm_tdx_corim,
122                igvm_snp_corim,
123                igvm_vbs_corim,
124                // Not part of completeness: the SNP ID block signing payload is
125                // an optional, SNP-only signing input, not a per-platform
126                // release endorsement.
127                igvm_snp_idblock: _,
128            } => {
129                igvm_tdx_json.is_some()
130                    && igvm_snp_json.is_some()
131                    && igvm_vbs_json.is_some()
132                    && igvm_tdx_corim.is_some()
133                    && igvm_snp_corim.is_some()
134                    && igvm_vbs_corim.is_some()
135            }
136        }
137    }
138}
139
140impl Artifact for OpenhclIgvmOutput {}
141
142impl OpenhclIgvmOutput {
143    pub fn igvm_bin(&self) -> &Path {
144        match self {
145            OpenhclIgvmOutput::LocalOnlyCustom { igvm_bin, .. }
146            | OpenhclIgvmOutput::X64 { igvm_bin }
147            | OpenhclIgvmOutput::X64Devkern { igvm_bin }
148            | OpenhclIgvmOutput::X64TestLinuxDirect { igvm_bin }
149            | OpenhclIgvmOutput::X64TestSivm { igvm_bin }
150            | OpenhclIgvmOutput::X64TestLinuxDirectDevkern { igvm_bin }
151            | OpenhclIgvmOutput::X64Cvm { igvm_bin, .. }
152            | OpenhclIgvmOutput::X64CvmDevkern { igvm_bin, .. }
153            | OpenhclIgvmOutput::Aarch64 { igvm_bin }
154            | OpenhclIgvmOutput::Aarch64Devkern { igvm_bin } => igvm_bin,
155        }
156    }
157
158    pub fn endorsements(&self) -> Option<&OpenhclIgvmEndorsements> {
159        match self {
160            OpenhclIgvmOutput::LocalOnlyCustom { endorsements, .. }
161            | OpenhclIgvmOutput::X64Cvm { endorsements, .. }
162            | OpenhclIgvmOutput::X64CvmDevkern { endorsements, .. } => endorsements.as_ref(),
163            _ => None,
164        }
165    }
166
167    pub fn recipe(&self) -> Option<OpenhclIgvmRecipe> {
168        match self {
169            OpenhclIgvmOutput::LocalOnlyCustom { .. } => None,
170            OpenhclIgvmOutput::X64 { .. } => Some(OpenhclIgvmRecipe::X64),
171            OpenhclIgvmOutput::X64Devkern { .. } => Some(OpenhclIgvmRecipe::X64Devkern),
172            OpenhclIgvmOutput::X64TestLinuxDirect { .. } => {
173                Some(OpenhclIgvmRecipe::X64TestLinuxDirect)
174            }
175            OpenhclIgvmOutput::X64TestSivm { .. } => Some(OpenhclIgvmRecipe::X64TestSivm),
176            OpenhclIgvmOutput::X64TestLinuxDirectDevkern { .. } => {
177                Some(OpenhclIgvmRecipe::X64TestLinuxDirectDevkern)
178            }
179            OpenhclIgvmOutput::X64Cvm { .. } => Some(OpenhclIgvmRecipe::X64Cvm),
180            OpenhclIgvmOutput::X64CvmDevkern { .. } => Some(OpenhclIgvmRecipe::X64CvmDevkern),
181            OpenhclIgvmOutput::Aarch64 { .. } => Some(OpenhclIgvmRecipe::Aarch64),
182            OpenhclIgvmOutput::Aarch64Devkern { .. } => Some(OpenhclIgvmRecipe::Aarch64Devkern),
183        }
184    }
185
186    pub fn new(recipe: Option<OpenhclIgvmRecipe>, igvm: IgvmOutput) -> Self {
187        let IgvmOutput {
188            igvm_bin,
189            igvm_map: _,
190            igvm_tdx_json,
191            igvm_snp_json,
192            igvm_vbs_json,
193            igvm_snp_idblock,
194            igvm_tdx_corim,
195            igvm_snp_corim,
196            igvm_vbs_corim,
197        } = igvm;
198        let mut endorsements = if igvm_tdx_json.is_some()
199            || igvm_snp_json.is_some()
200            || igvm_vbs_json.is_some()
201            || igvm_snp_idblock.is_some()
202        {
203            Some(OpenhclIgvmEndorsements::X64 {
204                igvm_tdx_json,
205                igvm_snp_json,
206                igvm_vbs_json,
207                igvm_snp_idblock,
208                igvm_tdx_corim,
209                igvm_snp_corim,
210                igvm_vbs_corim,
211            })
212        } else {
213            None
214        };
215        match recipe {
216            None => OpenhclIgvmOutput::LocalOnlyCustom {
217                igvm_bin,
218                endorsements,
219            },
220            Some(recipe) => {
221                let output = match recipe {
222                    OpenhclIgvmRecipe::X64 => OpenhclIgvmOutput::X64 { igvm_bin },
223                    OpenhclIgvmRecipe::X64Devkern => OpenhclIgvmOutput::X64Devkern { igvm_bin },
224                    OpenhclIgvmRecipe::X64TestLinuxDirect => {
225                        OpenhclIgvmOutput::X64TestLinuxDirect { igvm_bin }
226                    }
227                    OpenhclIgvmRecipe::X64TestSivm => OpenhclIgvmOutput::X64TestSivm { igvm_bin },
228                    OpenhclIgvmRecipe::X64TestLinuxDirectDevkern => {
229                        OpenhclIgvmOutput::X64TestLinuxDirectDevkern { igvm_bin }
230                    }
231                    OpenhclIgvmRecipe::X64Cvm => OpenhclIgvmOutput::X64Cvm {
232                        igvm_bin,
233                        endorsements: Some(
234                            endorsements
235                                .take()
236                                .filter(OpenhclIgvmEndorsements::is_complete)
237                                .expect("missing endorsements"),
238                        ),
239                    },
240                    OpenhclIgvmRecipe::X64CvmDevkern => OpenhclIgvmOutput::X64CvmDevkern {
241                        igvm_bin,
242                        endorsements: Some(
243                            endorsements
244                                .take()
245                                .filter(OpenhclIgvmEndorsements::is_complete)
246                                .expect("missing endorsements"),
247                        ),
248                    },
249                    OpenhclIgvmRecipe::Aarch64 => OpenhclIgvmOutput::Aarch64 { igvm_bin },
250                    OpenhclIgvmRecipe::Aarch64Devkern => {
251                        OpenhclIgvmOutput::Aarch64Devkern { igvm_bin }
252                    }
253                };
254                if endorsements.is_some() {
255                    panic!("unexpected endorsements");
256                }
257                output
258            }
259        }
260    }
261}
262
263/// OpenHCL IGVM extras output
264#[derive(Serialize, Deserialize)]
265pub struct OpenhclIgvmExtrasOutput {
266    #[serde(flatten)]
267    pub openhcl_boot: OpenhclBootOutput,
268    #[serde(flatten)]
269    pub openvmm_hcl: OpenvmmHclOutput,
270    #[serde(flatten)]
271    pub sidecar: Option<SidecarOutput>,
272    #[serde(rename = "openhcl.bin.map")]
273    pub igvm_map: Option<PathBuf>,
274}
275
276impl Artifact for OpenhclIgvmExtrasOutput {}
277
278#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
279pub enum OpenhclKernelPackage {
280    /// Kernel from the hcl-main branch
281    Main,
282    /// CVM kernel from the hcl-main branch
283    Cvm,
284    /// Kernel from the hcl-dev branch
285    Dev,
286    /// CVM kernel from the hcl-dev brnach
287    CvmDev,
288}
289
290/// Vtl0 kernel type
291#[derive(Serialize, Deserialize, Debug, Clone)]
292pub enum Vtl0KernelType {
293    Example,
294    LocalOnlyCustom(PathBuf),
295}
296
297#[derive(Serialize, Deserialize, Clone, Debug)]
298pub enum IgvmManifestPath {
299    /// Name of an in-tree manifest (located under `vm/loader/manifests`)
300    InTree(String),
301    /// An absolute path to a custom manifest (for local use only)
302    LocalOnlyCustom(PathBuf),
303}
304
305#[derive(Serialize, Deserialize, Clone, Debug)]
306pub struct OpenhclIgvmRecipeDetails {
307    pub local_only: Option<OpenhclIgvmRecipeDetailsLocalOnly>,
308
309    pub igvm_manifest: IgvmManifestPath,
310    pub openhcl_kernel_package: OpenhclKernelPackage,
311    pub openvmm_hcl_features: BTreeSet<OpenvmmHclFeature>,
312    pub target: CommonTriple,
313    pub vtl0_kernel_type: Option<Vtl0KernelType>,
314    pub with_uefi: bool,
315    pub with_interactive: bool,
316    pub with_sidecar: bool,
317    pub max_trace_level: MaxTraceLevel,
318}
319
320#[derive(Serialize, Deserialize, Clone, Debug)]
321pub struct OpenhclIgvmRecipeDetailsLocalOnly {
322    pub openvmm_hcl_no_strip: bool,
323    pub openhcl_initrd_extra_params: Option<OpenhclInitrdExtraParams>,
324    pub custom_openvmm_hcl: Option<PathBuf>,
325    pub custom_openhcl_boot: Option<PathBuf>,
326    pub custom_kernel: Option<PathBuf>,
327    pub custom_sidecar: Option<PathBuf>,
328    pub custom_extra_rootfs: Vec<PathBuf>,
329}
330
331#[expect(clippy::large_enum_variant)]
332#[derive(Debug, Serialize, Deserialize, Clone)]
333pub enum OpenhclIgvmRecipeType {
334    LocalOnlyCustom(OpenhclIgvmRecipeDetails),
335    WellKnown(OpenhclIgvmRecipe),
336}
337
338#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
339pub enum OpenhclIgvmRecipe {
340    X64,
341    X64Devkern,
342    X64TestLinuxDirect,
343    X64TestSivm,
344    X64TestLinuxDirectDevkern,
345    X64Cvm,
346    X64CvmDevkern,
347    Aarch64,
348    Aarch64Devkern,
349}
350
351impl ArtifactType for OpenhclIgvmRecipe {
352    fn name(&self, prefix: Option<&str>, suffix: Option<&str>) -> String {
353        [
354            Some(self.arch()),
355            prefix,
356            Some("openhcl"),
357            Some("igvm"),
358            self.flavor(),
359            suffix,
360        ]
361        .into_iter()
362        .flatten()
363        .collect::<Vec<_>>()
364        .join("-")
365    }
366}
367
368impl OpenhclIgvmRecipe {
369    pub fn common_arch(&self) -> CommonArch {
370        match self {
371            OpenhclIgvmRecipe::X64
372            | OpenhclIgvmRecipe::X64Devkern
373            | OpenhclIgvmRecipe::X64TestLinuxDirect
374            | OpenhclIgvmRecipe::X64TestSivm
375            | OpenhclIgvmRecipe::X64TestLinuxDirectDevkern
376            | OpenhclIgvmRecipe::X64Cvm
377            | OpenhclIgvmRecipe::X64CvmDevkern => CommonArch::X86_64,
378            OpenhclIgvmRecipe::Aarch64 | OpenhclIgvmRecipe::Aarch64Devkern => CommonArch::Aarch64,
379        }
380    }
381
382    pub fn uses_dev_kernel(&self) -> bool {
383        match self {
384            Self::X64
385            | Self::X64TestLinuxDirect
386            | Self::X64TestSivm
387            | Self::X64Cvm
388            | Self::Aarch64 => false,
389            Self::X64Devkern
390            | Self::X64TestLinuxDirectDevkern
391            | Self::X64CvmDevkern
392            | Self::Aarch64Devkern => true,
393        }
394    }
395
396    pub fn non_production_tag(&self) -> String {
397        let mut tag = self.arch().to_string();
398        if let Some(flavor) = self.flavor() {
399            tag.push('-');
400            tag.push_str(flavor);
401        }
402        tag
403    }
404
405    pub fn non_production_name(&self) -> String {
406        format!("openhcl-{}", self.non_production_tag())
407    }
408
409    fn flavor(&self) -> Option<&'static str> {
410        match self {
411            OpenhclIgvmRecipe::X64 | OpenhclIgvmRecipe::Aarch64 => None,
412            OpenhclIgvmRecipe::X64Devkern | OpenhclIgvmRecipe::Aarch64Devkern => Some("devkern"),
413            OpenhclIgvmRecipe::X64TestLinuxDirect => Some("test-linux-direct"),
414            OpenhclIgvmRecipe::X64TestSivm => Some("test-sivm"),
415            OpenhclIgvmRecipe::X64TestLinuxDirectDevkern => Some("test-linux-direct-devkern"),
416            OpenhclIgvmRecipe::X64Cvm => Some("cvm"),
417            OpenhclIgvmRecipe::X64CvmDevkern => Some("cvm-devkern"),
418        }
419    }
420
421    fn arch(&self) -> &'static str {
422        match self.common_arch() {
423            CommonArch::X86_64 => "x64",
424            CommonArch::Aarch64 => "aarch64",
425        }
426    }
427}
428
429impl OpenhclIgvmRecipeType {
430    pub fn recipe_details(&self, release_cfg: bool) -> OpenhclIgvmRecipeDetails {
431        match self {
432            Self::LocalOnlyCustom(details) => details.clone(),
433            Self::WellKnown(recipe) => recipe.recipe_details(release_cfg),
434        }
435    }
436
437    pub fn recipe(&self) -> Option<OpenhclIgvmRecipe> {
438        match self {
439            Self::LocalOnlyCustom(_) => None,
440            Self::WellKnown(recipe) => Some(recipe.clone()),
441        }
442    }
443}
444
445impl OpenhclIgvmRecipe {
446    pub fn recipe_details(&self, release_cfg: bool) -> OpenhclIgvmRecipeDetails {
447        let base_openvmm_hcl_features = || {
448            let mut m = BTreeSet::new();
449
450            m.insert(OpenvmmHclFeature::Tpm);
451
452            if !release_cfg {
453                m.insert(OpenvmmHclFeature::Gdb);
454            }
455
456            m
457        };
458
459        let in_repo_template = |debug_manifest: &'static str, release_manifest: &'static str| {
460            IgvmManifestPath::InTree(if release_cfg {
461                release_manifest.into()
462            } else {
463                debug_manifest.into()
464            })
465        };
466
467        // Debug configurations include --interactive by default, for busybox, gdbserver, and perf.
468        let with_interactive = !release_cfg;
469
470        // Save memory and cycles in hot paths by limiting the trace level in
471        // release builds.
472        let max_trace_level = if release_cfg {
473            MaxTraceLevel::Debug
474        } else {
475            MaxTraceLevel::Trace
476        };
477
478        match self {
479            Self::X64TestSivm => {
480                let mut details = Self::X64.recipe_details(release_cfg);
481                details.igvm_manifest =
482                    IgvmManifestPath::InTree("openhcl-x64-test-sivm.json".into());
483                details
484            }
485            Self::X64 => OpenhclIgvmRecipeDetails {
486                local_only: None,
487                igvm_manifest: in_repo_template("openhcl-x64-dev.json", "openhcl-x64-release.json"),
488                openhcl_kernel_package: OpenhclKernelPackage::Main,
489                openvmm_hcl_features: base_openvmm_hcl_features(),
490                target: CommonTriple::X86_64_LINUX_MUSL,
491                vtl0_kernel_type: None,
492                with_uefi: true,
493                with_interactive,
494                with_sidecar: true,
495                max_trace_level,
496            },
497            Self::X64Devkern => OpenhclIgvmRecipeDetails {
498                local_only: None,
499                igvm_manifest: in_repo_template("openhcl-x64-dev.json", "openhcl-x64-release.json"),
500                openhcl_kernel_package: OpenhclKernelPackage::Dev,
501                openvmm_hcl_features: base_openvmm_hcl_features(),
502                target: CommonTriple::X86_64_LINUX_MUSL,
503                vtl0_kernel_type: None,
504                with_uefi: true,
505                with_interactive,
506                with_sidecar: true,
507                max_trace_level,
508            },
509            Self::X64CvmDevkern => OpenhclIgvmRecipeDetails {
510                local_only: None,
511                igvm_manifest: in_repo_template(
512                    "openhcl-x64-cvm-dev.json",
513                    "openhcl-x64-cvm-release.json",
514                ),
515                openhcl_kernel_package: OpenhclKernelPackage::CvmDev,
516                openvmm_hcl_features: base_openvmm_hcl_features(),
517                target: CommonTriple::X86_64_LINUX_MUSL,
518                vtl0_kernel_type: None,
519                with_uefi: true,
520                with_interactive,
521                with_sidecar: false,
522                max_trace_level,
523            },
524            Self::X64TestLinuxDirect => OpenhclIgvmRecipeDetails {
525                local_only: None,
526                igvm_manifest: in_repo_template(
527                    "openhcl-x64-direct-dev.json",
528                    "openhcl-x64-direct-release.json",
529                ),
530                openhcl_kernel_package: OpenhclKernelPackage::Main,
531                openvmm_hcl_features: base_openvmm_hcl_features(),
532                target: CommonTriple::X86_64_LINUX_MUSL,
533                vtl0_kernel_type: Some(Vtl0KernelType::Example),
534                with_uefi: false,
535                with_interactive,
536                with_sidecar: true,
537                max_trace_level,
538            },
539            Self::X64TestLinuxDirectDevkern => OpenhclIgvmRecipeDetails {
540                local_only: None,
541                igvm_manifest: in_repo_template(
542                    "openhcl-x64-direct-dev.json",
543                    "openhcl-x64-direct-release.json",
544                ),
545                openhcl_kernel_package: OpenhclKernelPackage::Dev,
546                openvmm_hcl_features: base_openvmm_hcl_features(),
547                target: CommonTriple::X86_64_LINUX_MUSL,
548                vtl0_kernel_type: Some(Vtl0KernelType::Example),
549                with_uefi: false,
550                with_interactive,
551                with_sidecar: true,
552                max_trace_level,
553            },
554            Self::X64Cvm => OpenhclIgvmRecipeDetails {
555                local_only: None,
556                igvm_manifest: in_repo_template(
557                    "openhcl-x64-cvm-dev.json",
558                    "openhcl-x64-cvm-release.json",
559                ),
560                openhcl_kernel_package: OpenhclKernelPackage::Cvm,
561                openvmm_hcl_features: base_openvmm_hcl_features(),
562                target: CommonTriple::X86_64_LINUX_MUSL,
563                vtl0_kernel_type: None,
564                with_uefi: true,
565                with_interactive,
566                with_sidecar: false,
567                max_trace_level,
568            },
569            Self::Aarch64 => OpenhclIgvmRecipeDetails {
570                local_only: None,
571                igvm_manifest: in_repo_template(
572                    "openhcl-aarch64-dev.json",
573                    "openhcl-aarch64-release.json",
574                ),
575                openhcl_kernel_package: OpenhclKernelPackage::Main,
576                openvmm_hcl_features: base_openvmm_hcl_features(),
577                target: CommonTriple::AARCH64_LINUX_MUSL,
578                vtl0_kernel_type: None,
579                with_uefi: true,
580                with_interactive: false, // #1234
581                with_sidecar: false,
582                max_trace_level,
583            },
584            Self::Aarch64Devkern => OpenhclIgvmRecipeDetails {
585                local_only: None,
586                igvm_manifest: in_repo_template(
587                    "openhcl-aarch64-dev.json",
588                    "openhcl-aarch64-release.json",
589                ),
590                openhcl_kernel_package: OpenhclKernelPackage::Dev,
591                openvmm_hcl_features: base_openvmm_hcl_features(),
592                target: CommonTriple::AARCH64_LINUX_MUSL,
593                vtl0_kernel_type: None,
594                with_uefi: true,
595                with_interactive: false, // #1234
596                with_sidecar: false,
597                max_trace_level,
598            },
599        }
600    }
601}
602
603flowey_request! {
604    pub struct Request {
605        pub build_profile: OpenvmmHclBuildProfile,
606        pub release_cfg: bool,
607        pub recipe: OpenhclIgvmRecipeType,
608        pub custom_target: Option<CommonTriple>,
609        /// Additional features to enable on top of the recipe's defaults.
610        pub extra_features: BTreeSet<OpenvmmHclFeature>,
611        pub disable_secure_avic: bool,
612        pub uefi_firmware_flavor: Option<crate::download_uefi_mu_msvm::FirmwareFlavor>,
613        /// Add the confidential debug flag to the measured OpenHCL command
614        /// line, enabling confidential diagnostics on CVM builds.
615        pub confidential_debug: bool,
616
617        pub openhcl_igvm: WriteVar<OpenhclIgvmOutput>,
618        pub openhcl_igvm_extras: WriteVar<OpenhclIgvmExtrasOutput>,
619    }
620}
621
622new_simple_flow_node!(struct Node);
623
624impl SimpleFlowNode for Node {
625    type Request = Request;
626
627    fn imports(ctx: &mut ImportCtx<'_>) {
628        ctx.import::<crate::build_igvmfilegen::Node>();
629        ctx.import::<crate::build_openhcl_boot::Node>();
630        ctx.import::<crate::build_openhcl_initrd::Node>();
631        ctx.import::<crate::build_openvmm_hcl::Node>();
632        ctx.import::<crate::build_sidecar::Node>();
633        ctx.import::<crate::resolve_openhcl_kernel_package::Node>();
634        ctx.import::<crate::resolve_openvmm_deps::Node>();
635        ctx.import::<crate::resolve_openvmm_test_initrd::Node>();
636        ctx.import::<crate::resolve_openvmm_test_linux_kernel::Node>();
637        ctx.import::<crate::download_uefi_mu_msvm::Node>();
638        ctx.import::<crate::git_checkout_openvmm_repo::Node>();
639        ctx.import::<crate::run_igvmfilegen::Node>();
640        ctx.import::<crate::run_split_debug_info::Node>();
641    }
642
643    fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
644        let Request {
645            build_profile,
646            release_cfg,
647            recipe,
648            custom_target,
649            extra_features,
650            disable_secure_avic,
651            uefi_firmware_flavor,
652            confidential_debug,
653            openhcl_igvm,
654            openhcl_igvm_extras,
655        } = request;
656
657        let OpenhclIgvmRecipeDetails {
658            local_only,
659            igvm_manifest,
660            openhcl_kernel_package,
661            mut openvmm_hcl_features,
662            target,
663            vtl0_kernel_type,
664            with_uefi,
665            with_interactive,
666            with_sidecar,
667            max_trace_level,
668        } = recipe.recipe_details(release_cfg);
669
670        openvmm_hcl_features.extend(extra_features);
671
672        if disable_secure_avic {
673            openvmm_hcl_features.insert(OpenvmmHclFeature::LocalOnlyCustom(
674                "disable_secure_avic".into(),
675            ));
676        }
677
678        let OpenhclIgvmRecipeDetailsLocalOnly {
679            openvmm_hcl_no_strip,
680            openhcl_initrd_extra_params,
681            custom_openvmm_hcl,
682            custom_openhcl_boot,
683            custom_kernel,
684            custom_sidecar,
685            custom_extra_rootfs,
686        } = local_only.unwrap_or(OpenhclIgvmRecipeDetailsLocalOnly {
687            openvmm_hcl_no_strip: false,
688            openhcl_initrd_extra_params: None,
689            custom_openvmm_hcl: None,
690            custom_openhcl_boot: None,
691            custom_kernel: None,
692            custom_sidecar: None,
693            custom_extra_rootfs: Vec::new(),
694        });
695
696        let target = custom_target.unwrap_or(target);
697        let arch = CommonArch::from_triple(&target.as_triple())
698            .with_context(|| format!("cannot build openHCL from recipe on {target}"))?;
699
700        let openvmm_repo_path = ctx.reqv(crate::git_checkout_openvmm_repo::req::GetRepoDir);
701
702        let kernel_kind = match openhcl_kernel_package {
703            OpenhclKernelPackage::Main => OpenhclKernelPackageKind::Main,
704            OpenhclKernelPackage::Cvm => OpenhclKernelPackageKind::Cvm,
705            OpenhclKernelPackage::Dev => OpenhclKernelPackageKind::Dev,
706            OpenhclKernelPackage::CvmDev => OpenhclKernelPackageKind::CvmDev,
707        };
708
709        // Get the kernel package root for initrd building (needs metadata)
710        let vtl2_kernel_package_root =
711            ctx.reqv(
712                |v| crate::resolve_openhcl_kernel_package::Request::GetPackageRoot {
713                    kind: kernel_kind,
714                    arch,
715                    pkg: v,
716                },
717            );
718
719        // Get the modules path from the resolve node
720        let vtl2_kernel_modules =
721            ctx.reqv(
722                |v| crate::resolve_openhcl_kernel_package::Request::GetModules {
723                    kind: kernel_kind,
724                    arch,
725                    modules: v,
726                },
727            );
728
729        // Get the kernel metadata path from the resolve node
730        let vtl2_kernel_metadata =
731            ctx.reqv(
732                |v| crate::resolve_openhcl_kernel_package::Request::GetMetadata {
733                    kind: kernel_kind,
734                    arch,
735                    metadata: v,
736                },
737            );
738
739        let uefi_resource: Option<UefiResource> = with_uefi.then(|| UefiResource {
740            msvm_fd: ctx.reqv(|msvm_fd| crate::download_uefi_mu_msvm::Request::GetMsvmFd {
741                arch,
742                flavor: uefi_firmware_flavor,
743                msvm_fd,
744            }),
745        });
746
747        let vtl0_kernel_resource = vtl0_kernel_type.map(|typ| {
748            let kernel = if let Vtl0KernelType::LocalOnlyCustom(path) = typ {
749                ReadVar::from_static(path)
750            } else {
751                match typ {
752                    Vtl0KernelType::Example => ctx.reqv(|v| {
753                        crate::resolve_openvmm_test_linux_kernel::Request::Get(
754                            crate::resolve_openvmm_test_linux_kernel::OpenvmmTestKernelFile::Kernel,
755                            arch,
756                            crate::resolve_openvmm_test_linux_kernel::DEFAULT_LINUX_TEST_KERNEL_VERSION,
757                            v,
758                        )
759                    }),
760                    Vtl0KernelType::LocalOnlyCustom(_) => unreachable!("special cased above"),
761                }
762            };
763
764            let initrd = ctx.reqv(|v| {
765                crate::resolve_openvmm_test_initrd::Request::Get(arch, v)
766            });
767
768            Vtl0KernelResource { kernel, initrd }
769        });
770
771        // build sidecar
772        let sidecar = if with_sidecar {
773            let sidecar_bin = if let Some(path) = custom_sidecar {
774                ctx.emit_rust_stepv("set custom_sidecar", |_ctx| {
775                    |_rt| {
776                        let fake_dbg_path = std::env::current_dir()?
777                            .join("fake_sidecar.dbg")
778                            .absolute()?;
779                        fs_err::write(&fake_dbg_path, "")?;
780
781                        Ok(SidecarOutput {
782                            bin: path,
783                            dbg: fake_dbg_path,
784                        })
785                    }
786                })
787            } else {
788                ctx.reqv(|v| crate::build_sidecar::Request {
789                    build_params: crate::build_sidecar::SidecarBuildParams {
790                        arch,
791                        profile: match build_profile {
792                            OpenvmmHclBuildProfile::Debug => {
793                                crate::build_sidecar::SidecarBuildProfile::Debug
794                            }
795                            OpenvmmHclBuildProfile::Release
796                            | OpenvmmHclBuildProfile::OpenvmmHclShip => {
797                                crate::build_sidecar::SidecarBuildProfile::Release
798                            }
799                        },
800                    },
801                    sidecar: v,
802                })
803            };
804            Some(sidecar_bin)
805        } else {
806            None
807        };
808
809        // build openvmm_hcl bin
810        let openvmm_hcl = if let Some(ref path) = custom_openvmm_hcl {
811            let path = path.clone();
812            ctx.emit_rust_stepv("set custom_openvmm_hcl", |_ctx| {
813                |_rt| {
814                    Ok(OpenvmmHclOutput {
815                        bin: path,
816                        dbg: None,
817                    })
818                }
819            })
820        } else {
821            ctx.reqv(|v| {
822                crate::build_openvmm_hcl::Request {
823                    build_params: crate::build_openvmm_hcl::OpenvmmHclBuildParams {
824                        target: target.clone(),
825                        profile: build_profile,
826                        features: openvmm_hcl_features,
827                        // manually strip later, depending on provided igvm flags
828                        no_split_dbg_info: true,
829                        max_trace_level,
830                    },
831                    openvmm_hcl_output: v,
832                }
833            })
834        };
835
836        // build igvmfilegen (always built for host arch)
837        let igvmfilegen_arch: CommonArch = ctx.arch().try_into()?;
838
839        let igvmfilegen = ctx.reqv(|v| crate::build_igvmfilegen::Request {
840            build_params: crate::build_igvmfilegen::IgvmfilegenBuildParams {
841                target: CommonTriple::Common {
842                    arch: igvmfilegen_arch,
843                    platform: CommonPlatform::LinuxGnu,
844                },
845                profile: BuildProfile::Light,
846            },
847            igvmfilegen: v,
848        });
849
850        // build openhcl_boot
851        let openhcl_boot = if let Some(path) = custom_openhcl_boot {
852            ctx.emit_rust_stepv("set custom_openhcl_boot", |_ctx| {
853                |_rt| {
854                    let fake_dbg_path = std::env::current_dir()?.join("fake.dbg").absolute()?;
855                    fs_err::write(&fake_dbg_path, "")?;
856
857                    Ok(OpenhclBootOutput {
858                        bin: path,
859                        dbg: fake_dbg_path,
860                    })
861                }
862            })
863        } else {
864            ctx.reqv(|v| crate::build_openhcl_boot::Request {
865                build_params: crate::build_openhcl_boot::OpenhclBootBuildParams {
866                    arch,
867                    profile: match build_profile {
868                        OpenvmmHclBuildProfile::Debug => {
869                            crate::build_openhcl_boot::OpenhclBootBuildProfile::Debug
870                        }
871                        OpenvmmHclBuildProfile::Release
872                        | OpenvmmHclBuildProfile::OpenvmmHclShip => {
873                            crate::build_openhcl_boot::OpenhclBootBuildProfile::Release
874                        }
875                    },
876                },
877                openhcl_boot: v,
878            })
879        };
880
881        let use_stripped_openvmm_hcl = {
882            if custom_openvmm_hcl.is_some() {
883                // trust the user knows what they are doing if they specified a
884                // custom bin
885                false
886            } else {
887                !openvmm_hcl_no_strip
888            }
889        };
890
891        // use the stripped or unstripped openvmm_hcl as requested
892        let openvmm_hcl = if use_stripped_openvmm_hcl {
893            let (read, write) = ctx.new_var();
894            let (read_dbg, write_dbg) = ctx.new_var();
895
896            let in_bin = openvmm_hcl.map(ctx, |o| o.bin);
897            ctx.req(crate::run_split_debug_info::Request {
898                arch,
899                in_bin,
900                out_bin: write,
901                out_dbg_info: write_dbg,
902                reproducible_without_debuglink: matches!(
903                    ctx.platform(),
904                    FlowPlatform::Linux(FlowPlatformLinuxDistro::Nix)
905                ),
906            });
907
908            read.zip(ctx, read_dbg)
909                .map(ctx, |(bin, dbg)| OpenvmmHclOutput {
910                    bin,
911                    dbg: Some(dbg),
912                })
913        } else {
914            openvmm_hcl
915        };
916
917        let initrd = {
918            let rootfs_config = [openvmm_repo_path.map(ctx, |p| p.join("openhcl/rootfs.config"))]
919                .into_iter()
920                .chain(
921                    custom_extra_rootfs
922                        .into_iter()
923                        .map(|p| ReadVar::from_static(p)),
924                )
925                .collect();
926            let openvmm_hcl_bin = openvmm_hcl.map(ctx, |o| o.bin);
927
928            ctx.reqv(|v| crate::build_openhcl_initrd::Request {
929                interactive: with_interactive,
930                arch,
931                extra_params: openhcl_initrd_extra_params,
932                rootfs_config,
933                extra_env: None,
934                kernel_package_root: vtl2_kernel_package_root.clone(),
935                kernel_modules: vtl2_kernel_modules,
936                kernel_metadata: vtl2_kernel_metadata,
937                bin_openhcl: openvmm_hcl_bin,
938                initrd: v,
939            })
940        };
941
942        let kernel = if let Some(path) = custom_kernel {
943            ReadVar::from_static(path)
944        } else {
945            ctx.reqv(
946                |v| crate::resolve_openhcl_kernel_package::Request::GetKernel {
947                    kind: kernel_kind,
948                    arch,
949                    kernel: v,
950                },
951            )
952        };
953
954        let sidecar_bin = sidecar.clone().map(|x| x.map(ctx, |y| y.bin));
955        let openhcl_boot_bin = openhcl_boot.map(ctx, |x| x.bin);
956        let resources = ctx.emit_minor_rust_stepv("enumerate igvm resources", |ctx| {
957            claim_vars!(
958                ctx,
959                (
960                    initrd,
961                    kernel,
962                    openhcl_boot_bin,
963                    sidecar_bin,
964                    uefi_resource,
965                    vtl0_kernel_resource
966                )
967            );
968            |rt| {
969                let mut resources = BTreeMap::<ResourceType, PathBuf>::new();
970                resources.insert(ResourceType::UnderhillKernel, rt.read(kernel));
971                resources.insert(ResourceType::UnderhillInitrd, rt.read(initrd).initrd);
972                resources.insert(ResourceType::OpenhclBoot, rt.read(openhcl_boot_bin));
973                if let Some(sidecar_bin) = sidecar_bin {
974                    resources.insert(ResourceType::UnderhillSidecar, rt.read(sidecar_bin));
975                }
976                if let Some(uefi_resource) = uefi_resource {
977                    uefi_resource.add_to_resources(&mut resources, rt);
978                }
979                if let Some(vtl0_kernel_resource) = vtl0_kernel_resource {
980                    vtl0_kernel_resource.add_to_resources(&mut resources, rt);
981                }
982                resources
983            }
984        });
985
986        let igvmfilegen = igvmfilegen.map(ctx, |o| match o {
987            crate::build_igvmfilegen::IgvmfilegenOutput::LinuxBin { bin, dbg: _ } => bin,
988            crate::build_igvmfilegen::IgvmfilegenOutput::WindowsBin { exe, pdb: _ } => exe,
989        });
990
991        let manifest = match igvm_manifest {
992            IgvmManifestPath::InTree(path) => {
993                openvmm_repo_path.map(ctx, |p| p.join("vm/loader/manifests").join(path))
994            }
995            IgvmManifestPath::LocalOnlyCustom(p) => ReadVar::from_static(p),
996        };
997
998        let igvm = ctx.reqv(|v| crate::run_igvmfilegen::Request {
999            igvmfilegen,
1000            manifest,
1001            resources,
1002            disable_secure_avic,
1003            confidential_debug,
1004            // Open-source builds embed a temporary-key SNP ID block so the file
1005            // launches with `id_block_en = 1`; production pipelines add a
1006            // real-key ID block out-of-band instead.
1007            add_temp_snp_id_block: true,
1008            igvm: v,
1009        });
1010
1011        let igvm_map = igvm.map(ctx, |x| x.igvm_map);
1012        ctx.emit_minor_rust_step("construct openhcl extras", move |ctx| {
1013            claim_vars!(
1014                ctx,
1015                (
1016                    openhcl_boot,
1017                    openvmm_hcl,
1018                    sidecar,
1019                    igvm_map,
1020                    openhcl_igvm_extras
1021                )
1022            );
1023
1024            |rt| {
1025                read_vars!(rt, (openhcl_boot, openvmm_hcl, sidecar, igvm_map));
1026
1027                rt.write(
1028                    openhcl_igvm_extras,
1029                    &OpenhclIgvmExtrasOutput {
1030                        openhcl_boot,
1031                        openvmm_hcl,
1032                        sidecar,
1033                        igvm_map,
1034                    },
1035                );
1036            }
1037        });
1038
1039        igvm.write_into_with(ctx, openhcl_igvm, move |igvm| {
1040            OpenhclIgvmOutput::new(recipe.recipe(), igvm)
1041        });
1042
1043        Ok(())
1044    }
1045}
1046
1047#[derive(Debug)]
1048pub struct UefiResource<C = VarNotClaimed> {
1049    pub msvm_fd: ReadVar<PathBuf, C>,
1050}
1051
1052impl ClaimVar for UefiResource {
1053    type Claimed = UefiResource<VarClaimed>;
1054
1055    fn claim(self, ctx: &mut StepCtx<'_>) -> UefiResource<VarClaimed> {
1056        UefiResource {
1057            msvm_fd: self.msvm_fd.claim(ctx),
1058        }
1059    }
1060}
1061
1062impl UefiResource<VarClaimed> {
1063    pub fn add_to_resources(
1064        self,
1065        resources: &mut BTreeMap<ResourceType, PathBuf>,
1066        rt: &mut RustRuntimeServices<'_>,
1067    ) {
1068        let path = rt.read(self.msvm_fd);
1069        resources.insert(ResourceType::Uefi, path);
1070    }
1071}
1072
1073pub struct Vtl0KernelResource<C = VarNotClaimed> {
1074    pub kernel: ReadVar<PathBuf, C>,
1075    pub initrd: ReadVar<PathBuf, C>,
1076}
1077
1078impl ClaimVar for Vtl0KernelResource {
1079    type Claimed = Vtl0KernelResource<VarClaimed>;
1080
1081    fn claim(self, ctx: &mut StepCtx<'_>) -> Vtl0KernelResource<VarClaimed> {
1082        Vtl0KernelResource {
1083            kernel: self.kernel.claim(ctx),
1084            initrd: self.initrd.claim(ctx),
1085        }
1086    }
1087}
1088
1089impl Vtl0KernelResource<VarClaimed> {
1090    pub fn add_to_resources(
1091        self,
1092        resources: &mut BTreeMap<ResourceType, PathBuf>,
1093        rt: &mut RustRuntimeServices<'_>,
1094    ) {
1095        let kernel = rt.read(self.kernel);
1096        let initrd = rt.read(self.initrd);
1097        resources.insert(ResourceType::LinuxKernel, kernel);
1098        resources.insert(ResourceType::LinuxInitrd, initrd);
1099    }
1100}