Skip to main content

flowey_lib_hvlite/
resolve_openhcl_kernel_package.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Resolve OpenHCL kernel packages - either by downloading from GitHub Release
5//! or using local paths
6
7use crate::common::CommonArch;
8use flowey::node::prelude::*;
9use flowey_lib_common::_util::group_by;
10use std::collections::BTreeMap;
11
12#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
13pub enum OpenhclKernelPackageKind {
14    Main,
15    Cvm,
16    Dev,
17    CvmDev,
18}
19
20impl OpenhclKernelPackageKind {
21    pub fn is_dev(self) -> bool {
22        match self {
23            Self::Main | Self::Cvm => false,
24            Self::Dev | Self::CvmDev => true,
25        }
26    }
27}
28
29flowey_config! {
30    /// Config for the resolve_openhcl_kernel_package node.
31    pub struct Config {
32        /// Version strings keyed by package kind.
33        pub versions: BTreeMap<OpenhclKernelPackageKind, String>,
34        /// Local paths keyed by architecture (kernel binary, modules directory).
35        pub local_paths: BTreeMap<CommonArch, (ConfigVar<PathBuf>, ConfigVar<PathBuf>)>,
36    }
37}
38
39flowey_request! {
40    #[expect(clippy::enum_variant_names)]
41    pub enum Request {
42        /// Get path to the kernel binary
43        GetKernel {
44            kind: OpenhclKernelPackageKind,
45            arch: CommonArch,
46            kernel: WriteVar<PathBuf>,
47        },
48        /// Get path to the kernel modules directory
49        GetModules {
50            kind: OpenhclKernelPackageKind,
51            arch: CommonArch,
52            modules: WriteVar<PathBuf>,
53        },
54        /// Get path to the package root (for metadata files, etc)
55        GetPackageRoot {
56            kind: OpenhclKernelPackageKind,
57            arch: CommonArch,
58            pkg: WriteVar<PathBuf>,
59        },
60        /// Get path to the kernel build metadata file
61        GetMetadata {
62            kind: OpenhclKernelPackageKind,
63            arch: CommonArch,
64            metadata: WriteVar<PathBuf>,
65        },
66    }
67}
68
69new_flow_node_with_config!(struct Node);
70
71impl FlowNodeWithConfig for Node {
72    type Request = Request;
73    type Config = Config;
74
75    fn imports(ctx: &mut ImportCtx<'_>) {
76        ctx.import::<flowey_lib_common::install_dist_pkg::Node>();
77        ctx.import::<flowey_lib_common::download_gh_release::Node>();
78    }
79
80    fn emit(
81        config: Config,
82        requests: Vec<Self::Request>,
83        ctx: &mut NodeCtx<'_>,
84    ) -> anyhow::Result<()> {
85        let versions = config.versions;
86        let local_paths = config.local_paths;
87        let mut kernel_reqs: BTreeMap<
88            (OpenhclKernelPackageKind, CommonArch),
89            Vec<WriteVar<PathBuf>>,
90        > = BTreeMap::new();
91        let mut modules_reqs: BTreeMap<
92            (OpenhclKernelPackageKind, CommonArch),
93            Vec<WriteVar<PathBuf>>,
94        > = BTreeMap::new();
95        let mut pkg_reqs: BTreeMap<(OpenhclKernelPackageKind, CommonArch), Vec<WriteVar<PathBuf>>> =
96            BTreeMap::new();
97        let mut metadata_reqs: BTreeMap<
98            (OpenhclKernelPackageKind, CommonArch),
99            Vec<WriteVar<PathBuf>>,
100        > = BTreeMap::new();
101
102        for req in requests {
103            match req {
104                Request::GetKernel { kind, arch, kernel } => {
105                    kernel_reqs.entry((kind, arch)).or_default().push(kernel);
106                }
107                Request::GetModules {
108                    kind,
109                    arch,
110                    modules,
111                } => {
112                    modules_reqs.entry((kind, arch)).or_default().push(modules);
113                }
114                Request::GetPackageRoot { kind, arch, pkg } => {
115                    pkg_reqs.entry((kind, arch)).or_default().push(pkg);
116                }
117                Request::GetMetadata {
118                    kind,
119                    arch,
120                    metadata,
121                } => {
122                    metadata_reqs
123                        .entry((kind, arch))
124                        .or_default()
125                        .push(metadata);
126                }
127            }
128        }
129
130        // Collect all architectures that need resolution
131        let all_reqs: std::collections::BTreeSet<(OpenhclKernelPackageKind, CommonArch)> =
132            kernel_reqs
133                .keys()
134                .chain(modules_reqs.keys())
135                .chain(pkg_reqs.keys())
136                .chain(metadata_reqs.keys())
137                .copied()
138                .collect();
139
140        // Verify we have either local paths or versions for each requested architecture
141        for (kind, arch) in &all_reqs {
142            if !local_paths.contains_key(arch) && !versions.contains_key(kind) {
143                if kind.is_dev() {
144                    anyhow::bail!(
145                        "OpenHCL dev kernel support is disabled; provide local kernel paths for \
146                         {:?} to enable {:?}",
147                        arch,
148                        kind,
149                    );
150                }
151                anyhow::bail!(
152                    "Must provide either SetLocal for {:?} or SetVersion for {:?}",
153                    arch,
154                    kind
155                );
156            }
157        }
158
159        if all_reqs.is_empty() {
160            return Ok(());
161        }
162
163        // Partition requests into local vs download
164        let (local_reqs, download_reqs): (Vec<_>, Vec<_>) = all_reqs
165            .into_iter()
166            .partition(|(_, arch)| local_paths.contains_key(arch));
167
168        // Split the request maps into local and download portions
169        let (kernel_reqs_local, mut kernel_reqs_download): (BTreeMap<_, _>, BTreeMap<_, _>) =
170            kernel_reqs
171                .into_iter()
172                .partition(|((_, arch), _)| local_paths.contains_key(arch));
173        let (modules_reqs_local, mut modules_reqs_download): (BTreeMap<_, _>, BTreeMap<_, _>) =
174            modules_reqs
175                .into_iter()
176                .partition(|((_, arch), _)| local_paths.contains_key(arch));
177        let (pkg_reqs_local, mut pkg_reqs_download): (BTreeMap<_, _>, BTreeMap<_, _>) = pkg_reqs
178            .into_iter()
179            .partition(|((_, arch), _)| local_paths.contains_key(arch));
180        let (metadata_reqs_local, mut metadata_reqs_download): (BTreeMap<_, _>, BTreeMap<_, _>) =
181            metadata_reqs
182                .into_iter()
183                .partition(|((_, arch), _)| local_paths.contains_key(arch));
184
185        // Handle local paths
186        if !local_reqs.is_empty() {
187            ctx.emit_rust_step("use local kernel package", |ctx| {
188                let mut kernel_reqs = kernel_reqs_local.claim(ctx);
189                let mut modules_reqs = modules_reqs_local.claim(ctx);
190                let mut pkg_reqs = pkg_reqs_local.claim(ctx);
191                let mut metadata_reqs = metadata_reqs_local.claim(ctx);
192                let local_paths: BTreeMap<_, _> = local_paths
193                    .into_iter()
194                    .map(|(arch, (k, m))| (arch, (k.claim(ctx), m.claim(ctx))))
195                    .collect();
196                let local_reqs = group_by(local_reqs.into_iter().map(|(kind, arch)| (arch, kind)));
197
198                move |rt| {
199                    for (arch, kinds) in local_reqs {
200                        let (kernel_var, modules_var) = local_paths
201                            .get(&arch)
202                            .with_context(|| format!("missing local kernel paths for {arch:?}"))?;
203                        let kernel_path = rt.read(kernel_var.clone());
204                        let modules_path = rt.read(modules_var.clone());
205
206                        log::info!(
207                            "using local kernel at {:?} and modules at {:?}",
208                            kernel_path,
209                            modules_path
210                        );
211
212                        let package_root = kernel_path.parent().map(Path::to_path_buf);
213                        let metadata_path = package_root
214                            .as_ref()
215                            .map(|root| root.join("kernel_build_metadata.json"));
216                        for kind in kinds {
217                            if let Some(vars) = kernel_reqs.remove(&(kind, arch)) {
218                                rt.write_all(vars, &kernel_path);
219                            }
220                            if let Some(vars) = modules_reqs.remove(&(kind, arch)) {
221                                rt.write_all(vars, &modules_path);
222                            }
223                            if let Some(vars) = pkg_reqs.remove(&(kind, arch)) {
224                                rt.write_all(
225                                    vars,
226                                    package_root
227                                        .as_ref()
228                                        .context("local kernel path has no package directory")?,
229                                );
230                            }
231                            if let Some(vars) = metadata_reqs.remove(&(kind, arch)) {
232                                rt.write_all(
233                                    vars,
234                                    metadata_path
235                                        .as_ref()
236                                        .context("local kernel path has no metadata directory")?,
237                                );
238                            }
239                        }
240                    }
241                    Ok(())
242                }
243            });
244        }
245
246        if download_reqs.is_empty() {
247            return Ok(());
248        }
249
250        // Handle downloads
251        let extract_zip_deps = flowey_lib_common::_util::extract::extract_zip_if_new_deps(ctx);
252
253        for (kind, arch) in download_reqs {
254            let version = versions
255                .get(&kind)
256                .with_context(|| format!("missing kernel package version for {kind:?}"))?;
257            let tag = format!(
258                "rolling-lts/hcl-{}/{}",
259                match kind {
260                    OpenhclKernelPackageKind::Main | OpenhclKernelPackageKind::Cvm => "main",
261                    OpenhclKernelPackageKind::Dev | OpenhclKernelPackageKind::CvmDev => "dev",
262                },
263                version
264            );
265
266            let file_name = format!(
267                "Microsoft.OHCL.Kernel{}.{}{}-{}.tar.gz",
268                match kind {
269                    OpenhclKernelPackageKind::Main | OpenhclKernelPackageKind::Cvm => "",
270                    OpenhclKernelPackageKind::Dev | OpenhclKernelPackageKind::CvmDev => ".Dev",
271                },
272                version,
273                match kind {
274                    OpenhclKernelPackageKind::Main | OpenhclKernelPackageKind::Dev => "",
275                    OpenhclKernelPackageKind::Cvm | OpenhclKernelPackageKind::CvmDev => "-cvm",
276                },
277                match arch {
278                    CommonArch::X86_64 => "x64",
279                    CommonArch::Aarch64 => "arm64",
280                },
281            );
282
283            let kernel_package_tar_gz =
284                ctx.reqv(|v| flowey_lib_common::download_gh_release::Request {
285                    repo_owner: crate::common::OPENVMM_GITHUB_OWNER.into(),
286                    repo_name: "OHCL-Linux-Kernel".into(),
287                    needs_auth: false,
288                    tag,
289                    file_name: file_name.clone(),
290                    path: v,
291                });
292
293            let kernel_file_name = match arch {
294                CommonArch::X86_64 => "vmlinux",
295                CommonArch::Aarch64 => "Image",
296            };
297
298            ctx.emit_rust_step("extract and resolve kernel package", |ctx| {
299                let extract_zip_deps = extract_zip_deps.clone().claim(ctx);
300                let kernel_vars = kernel_reqs_download.remove(&(kind, arch)).claim(ctx);
301                let modules_vars = modules_reqs_download.remove(&(kind, arch)).claim(ctx);
302                let pkg_vars = pkg_reqs_download.remove(&(kind, arch)).claim(ctx);
303                let metadata_vars = metadata_reqs_download.remove(&(kind, arch)).claim(ctx);
304                let kernel_package_tar_gz = kernel_package_tar_gz.claim(ctx);
305
306                move |rt| {
307                    let kernel_package_tar_gz = rt.read(kernel_package_tar_gz);
308
309                    // Extract the downloaded package
310                    let extract_dir = flowey_lib_common::_util::extract::extract_zip_if_new(
311                        rt,
312                        extract_zip_deps,
313                        &kernel_package_tar_gz,
314                        &file_name,
315                    )?;
316
317                    // The extracted directory contains: vmlinux/Image, modules/, kernel_build_metadata.json
318                    let kernel_path = extract_dir.join(kernel_file_name);
319                    let modules_path = extract_dir.join("modules");
320                    let metadata_path = extract_dir.join("kernel_build_metadata.json");
321
322                    if let Some(vars) = kernel_vars {
323                        rt.write_all(vars, &kernel_path);
324                    }
325                    if let Some(vars) = modules_vars {
326                        rt.write_all(vars, &modules_path);
327                    }
328                    if let Some(vars) = pkg_vars {
329                        rt.write_all(vars, &extract_dir);
330                    }
331                    if let Some(vars) = metadata_vars {
332                        rt.write_all(vars, &metadata_path);
333                    }
334
335                    Ok(())
336                }
337            });
338        }
339
340        Ok(())
341    }
342}