Skip to main content

flowey_lib_hvlite/
resolve_openvmm_qemu.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Download the statically-linked QEMU binary from the `openvmm-deps`
5//! GitHub release, or use a local path if specified.
6//!
7//! Each release publishes `qemu-linux-static.<host_arch>.<ver>.tar.gz`
8//! archives containing QEMU system-emulation binaries (e.g.,
9//! `qemu-system-aarch64`) built for the given host architecture.
10
11use crate::common::CommonArch;
12use flowey::node::prelude::*;
13use std::collections::BTreeMap;
14use std::collections::BTreeSet;
15
16/// Which QEMU binary to extract from the archive.
17#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
18pub enum QemuFile {
19    /// `qemu-system-aarch64` — emulates aarch64 guests.
20    SystemAarch64,
21}
22
23impl QemuFile {
24    /// The filename of this binary inside the archive.
25    pub fn filename(self) -> &'static str {
26        match self {
27            Self::SystemAarch64 => "qemu-system-aarch64",
28        }
29    }
30}
31
32flowey_config! {
33    /// Config for the resolve_openvmm_qemu node.
34    pub struct Config {
35        /// Specify version of the github release to pull from
36        pub version: Option<String>,
37        /// Use locally downloaded QEMU binaries, keyed by host architecture
38        pub local_paths: BTreeMap<CommonArch, ConfigVar<PathBuf>>,
39    }
40}
41
42flowey_request! {
43    pub enum Request {
44        /// Get the path to a QEMU binary for the given host architecture.
45        Get(QemuFile, CommonArch, WriteVar<PathBuf>),
46    }
47}
48
49new_flow_node_with_config!(struct Node);
50
51impl FlowNodeWithConfig for Node {
52    type Request = Request;
53    type Config = Config;
54
55    fn imports(ctx: &mut ImportCtx<'_>) {
56        ctx.import::<flowey_lib_common::install_dist_pkg::Node>();
57        ctx.import::<flowey_lib_common::download_gh_release::Node>();
58    }
59
60    fn emit(
61        config: Config,
62        requests: Vec<Self::Request>,
63        ctx: &mut NodeCtx<'_>,
64    ) -> anyhow::Result<()> {
65        let Config {
66            version,
67            local_paths,
68        } = config;
69        let mut deps: BTreeMap<(QemuFile, CommonArch), Vec<WriteVar<PathBuf>>> = BTreeMap::new();
70
71        for req in requests {
72            match req {
73                Request::Get(file, arch, var) => {
74                    deps.entry((file, arch)).or_default().push(var);
75                }
76            }
77        }
78
79        if version.is_some() && !local_paths.is_empty() {
80            anyhow::bail!("Cannot specify both Version and LocalPath requests");
81        }
82
83        if version.is_none() && local_paths.is_empty() {
84            anyhow::bail!("Must specify a Version or LocalPath request");
85        }
86
87        // -- end of req processing -- //
88
89        if deps.is_empty() {
90            return Ok(());
91        }
92
93        if !local_paths.is_empty() {
94            ctx.emit_rust_step("use local QEMU", |ctx| {
95                let deps = deps.claim(ctx);
96                let local_paths: BTreeMap<_, _> = local_paths
97                    .into_iter()
98                    .map(|(key, var)| (key, var.claim(ctx)))
99                    .collect();
100                move |rt| {
101                    let resolved_paths: BTreeMap<CommonArch, PathBuf> = local_paths
102                        .into_iter()
103                        .map(|(key, var)| (key, rt.read(var)))
104                        .collect();
105
106                    for ((file, arch), vars) in deps {
107                        let base_dir = resolved_paths.get(&arch).ok_or_else(|| {
108                            anyhow::anyhow!("No local path specified for {:?}", arch)
109                        })?;
110                        let path = base_dir.join(file.filename());
111                        rt.write_all(vars, &path)
112                    }
113
114                    Ok(())
115                }
116            });
117
118            return Ok(());
119        }
120
121        let version = version.expect("local requests handled above");
122
123        // Deduplicate downloads per host architecture.
124        let needed_archives: BTreeSet<CommonArch> = deps.keys().map(|(_, arch)| *arch).collect();
125
126        let mut archives = BTreeMap::new();
127        for arch in needed_archives {
128            let arch_str = match arch {
129                CommonArch::X86_64 => "x86_64",
130                CommonArch::Aarch64 => "aarch64",
131            };
132            let archive = ctx.reqv(|v| flowey_lib_common::download_gh_release::Request {
133                repo_owner: "microsoft".into(),
134                repo_name: "openvmm-deps".into(),
135                needs_auth: false,
136                tag: version.clone(),
137                file_name: format!("qemu-linux-static.{arch_str}.{version}.tar.gz"),
138                path: v,
139            });
140            archives.insert(arch, archive);
141        }
142
143        let persistent_dir = ctx.persistent_dir();
144
145        ctx.emit_rust_step("unpack QEMU archive", |ctx| {
146            let persistent_dir = persistent_dir.claim(ctx);
147            let archives = archives.claim(ctx);
148            let deps = deps.claim(ctx);
149            let version = version.clone();
150            move |rt| {
151                let persistent_dir = persistent_dir.map(|d| rt.read(d));
152
153                let mut extract_dirs = BTreeMap::new();
154                for (arch, archive) in archives {
155                    let file = rt.read(archive);
156                    let dir = flowey_lib_common::_util::extract::extract_tar_gz_if_new(
157                        rt,
158                        persistent_dir.as_deref(),
159                        &file,
160                        &version,
161                    )?;
162                    extract_dirs.insert(arch, dir);
163                }
164
165                for ((file, arch), vars) in deps {
166                    let extract_dir = extract_dirs
167                        .get(&arch)
168                        .expect("archive was downloaded for this arch");
169                    let path = extract_dir.join(file.filename());
170                    if !path.exists() {
171                        anyhow::bail!(
172                            "expected QEMU binary not found in archive: {}",
173                            path.display()
174                        );
175                    }
176                    // Ensure the binary is executable
177                    path.make_executable()?;
178                    rt.write_all(vars, &path)
179                }
180
181                Ok(())
182            }
183        });
184
185        Ok(())
186    }
187}