1use crate::common::CommonArch;
7use crate::common::CommonProfile;
8use crate::common::CommonTriple;
9use flowey::node::prelude::*;
10use sha2::Digest as _;
11
12flowey_request! {
13 pub struct Params {
14 pub profile: CommonProfile,
15 pub source_profile: String,
16 pub working_dir: PathBuf,
17 pub kernel: Option<PathBuf>,
18 pub initrd: Option<PathBuf>,
19 pub firmware: Option<PathBuf>,
20 pub boot_artifacts: Option<PathBuf>,
21 pub share: PathBuf,
22 pub dir: PathBuf,
23 pub kernel_cmdline: Option<String>,
24 pub hypervisor_cmdline: Option<String>,
25 pub prepare_only: bool,
26 pub incubator_args: Vec<String>,
27 pub done: WriteVar<SideEffect>,
28 }
29}
30
31new_simple_flow_node!(struct Node);
32
33impl SimpleFlowNode for Node {
34 type Request = Params;
35
36 fn imports(ctx: &mut ImportCtx<'_>) {
37 ctx.import::<crate::build_openvmm::Node>();
38 ctx.import::<crate::build_incubator::Node>();
39 ctx.import::<crate::build_pipette::Node>();
40 ctx.import::<crate::build_mshv_host_boot::Node>();
41 ctx.import::<crate::resolve_mshv_boot_artifacts::Node>();
42 ctx.import::<crate::resolve_openvmm_test_initrd::Node>();
43 ctx.import::<crate::resolve_openvmm_test_linux_kernel::Node>();
44 ctx.import::<crate::download_uefi_mu_msvm::Node>();
45 }
46
47 fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
48 let Params {
49 profile,
50 source_profile,
51 working_dir,
52 kernel,
53 initrd,
54 firmware,
55 boot_artifacts,
56 share,
57 dir,
58 kernel_cmdline,
59 hypervisor_cmdline,
60 prepare_only,
61 incubator_args,
62 done,
63 } = request;
64 ctx.config(crate::resolve_mshv_boot_artifacts::Config {
65 local_path: boot_artifacts.map(|path| ConfigVar(ReadVar::from_static(path))),
66 });
67 let openvmm = ctx.reqv(|openvmm| crate::build_openvmm::Request {
68 params: crate::build_openvmm::OpenvmmBuildParams {
69 target: CommonTriple::X86_64_LINUX_GNU,
70 profile,
71 features: Default::default(),
72 },
73 openvmm,
74 });
75 let incubator = ctx.reqv(|incubator| crate::build_incubator::Request {
76 target: CommonTriple::X86_64_LINUX_GNU,
77 profile: if profile.to_release() {
78 crate::run_cargo_build::BuildProfile::Release
79 } else {
80 crate::run_cargo_build::BuildProfile::Light
81 },
82 incubator,
83 });
84 let pipette = ctx.reqv(|pipette| crate::build_pipette::Request {
85 target: CommonTriple::X86_64_LINUX_MUSL,
86 profile,
87 pipette,
88 });
89 let host_boot =
90 ctx.reqv(|host_boot| crate::build_mshv_host_boot::Request { profile, host_boot });
91 let artifacts = ctx.reqv(|artifacts| crate::resolve_mshv_boot_artifacts::Request {
92 arch: CommonArch::X86_64,
93 artifacts,
94 });
95 let kernel = kernel.map(ReadVar::from_static).unwrap_or_else(|| {
96 ctx.reqv(|kernel| {
97 crate::resolve_openvmm_test_linux_kernel::Request::Get(
98 crate::resolve_openvmm_test_linux_kernel::OpenvmmTestKernelFile::BzImage,
99 CommonArch::X86_64,
100 crate::resolve_openvmm_test_linux_kernel::LinuxTestKernelVersion::MshvTest,
101 kernel,
102 )
103 })
104 });
105 let initrd = initrd.map(ReadVar::from_static).unwrap_or_else(|| {
106 ctx.reqv(|initrd| {
107 crate::resolve_openvmm_test_initrd::Request::Get(CommonArch::X86_64, initrd)
108 })
109 });
110 let firmware = firmware.map(ReadVar::from_static).unwrap_or_else(|| {
111 ctx.reqv(|msvm_fd| crate::download_uefi_mu_msvm::Request::GetMsvmFd {
112 arch: CommonArch::X86_64,
113 flavor: None,
114 msvm_fd,
115 })
116 });
117
118 ctx.emit_rust_step("prepare standalone Incubator profile and command", |ctx| {
119 let openvmm = openvmm.claim(ctx);
120 let incubator = incubator.claim(ctx);
121 let pipette = pipette.claim(ctx);
122 let host_boot = host_boot.claim(ctx);
123 let artifacts = artifacts.claim(ctx);
124 let kernel = kernel.claim(ctx);
125 let initrd = initrd.claim(ctx);
126 let firmware = firmware.claim(ctx);
127 done.claim(ctx);
128 move |rt| {
129 let runner = rt.read(incubator).bin;
130 let crate::build_openvmm::OpenvmmOutput::LinuxBin { bin: openvmm, .. } =
131 rt.read(openvmm)
132 else {
133 unreachable!()
134 };
135 let crate::build_pipette::PipetteOutput::LinuxBin { bin: pipette, .. } =
136 rt.read(pipette)
137 else {
138 unreachable!()
139 };
140 let host_boot = rt.read(host_boot).efi;
141 let artifacts = rt.read(artifacts);
142 let kernel = rt.read(kernel);
143 let firmware = rt.read(firmware);
144 let initrd = rt.read(initrd);
145 fs_err::create_dir_all(&dir)?;
146 let runtime = stage_runtime(
147 &dir.join("runtime"),
148 &[
149 ("openvmm", &openvmm),
150 ("incubator", &runner),
151 ("pipette", &pipette),
152 ("mshv_host_boot.efi", &host_boot),
153 ],
154 )?;
155 let openvmm = runtime.join("openvmm");
156 let runner = runtime.join("incubator");
157 let host_boot = runtime.join("mshv_host_boot.efi");
158 let pipette_path = runtime.join("pipette");
159 let relative_pipette = pipette_path.strip_prefix(&share)?;
160 let guest_pipette = format!("/share/{}", path_str(relative_pipette)?);
161
162 let config = resolve_profile(
163 &source_profile,
164 &openvmm,
165 &host_boot,
166 &firmware,
167 &artifacts,
168 kernel_cmdline.as_deref(),
169 hypervisor_cmdline.as_deref(),
170 )?;
171 let profile_path = dir.join("profile.toml");
172 let profile_file = tempfile::NamedTempFile::new_in(&dir)?;
173 fs_err::write(profile_file.path(), config)?;
174 profile_file.persist(&profile_path)?;
175 let args = vec![
176 "--profile".into(),
177 path_str(&profile_path)?.into(),
178 "--kernel".into(),
179 path_str(&kernel)?.into(),
180 "--initrd".into(),
181 path_str(&initrd)?.into(),
182 "--share".into(),
183 path_str(&share)?.into(),
184 "--guest-pipette".into(),
185 guest_pipette,
186 "--output-dir".into(),
187 path_str(&dir.join("results"))?.into(),
188 ];
189 log::info!("Prepared profile: {}", profile_path.display());
190 if prepare_only {
191 let command = std::iter::once(path_str(&runner)?)
192 .chain(args.iter().map(String::as_str))
193 .chain(["--", "/bin/sh"])
194 .map(shell_word)
195 .collect::<Vec<_>>()
196 .join(" ");
197 log::info!("Run directly: {command}");
198 } else {
199 rt.sh.change_dir(&working_dir);
200 flowey::shell_cmd!(rt, "{runner}")
201 .args(args)
202 .args(incubator_args)
203 .run_interactive()?;
204 }
205 Ok(())
206 }
207 });
208 Ok(())
209 }
210}
211
212pub fn parse_source_profile(contents: &str) -> anyhow::Result<toml_edit::DocumentMut> {
214 let config: toml_edit::DocumentMut = contents.parse()?;
215 let incubator = config.get("incubator").context("missing [incubator]")?;
216 let boot = config.get("boot").context("missing [boot]")?;
217 anyhow::ensure!(
218 incubator.get("type").and_then(|v| v.as_str()) == Some("openvmm")
219 && incubator.get("arch").and_then(|v| v.as_str()) == Some("x86-64")
220 && boot.get("type").and_then(|v| v.as_str()) == Some("mshv"),
221 "Incubator provisioning currently supports only type=openvmm, arch=x86-64, boot.type=mshv profiles"
222 );
223 Ok(config)
224}
225
226fn resolve_profile(
227 source: &str,
228 openvmm: &Path,
229 host_boot: &Path,
230 firmware: &Path,
231 artifacts: &Path,
232 kernel_cmdline: Option<&str>,
233 hypervisor_cmdline: Option<&str>,
234) -> anyhow::Result<String> {
235 let mut config = parse_source_profile(source)?;
236 config["incubator"]["binary"] = toml_edit::value(path_str(openvmm)?);
237 config["boot"]["host-boot"] = toml_edit::value(path_str(host_boot)?);
238 config["boot"]["firmware"] = toml_edit::value(path_str(firmware)?);
239 config["boot"]["boot-artifacts"] = toml_edit::value(path_str(artifacts)?);
240 if let Some(value) = kernel_cmdline {
241 config["boot"]["kernel-cmdline"] = toml_edit::value(value);
242 }
243 if let Some(value) = hypervisor_cmdline {
244 config["boot"]["hypervisor-cmdline"] = toml_edit::value(value);
245 }
246 Ok(config.to_string())
247}
248
249fn path_str(path: &Path) -> anyhow::Result<&str> {
250 path.to_str()
251 .with_context(|| format!("path is not valid UTF-8: {}", path.display()))
252}
253
254fn shell_word(word: &str) -> String {
255 format!("'{}'", word.replace('\'', "'\\''"))
256}
257
258fn stage_runtime(root: &Path, files: &[(&str, &Path)]) -> anyhow::Result<PathBuf> {
261 let mut hash = sha2::Sha256::new();
262 let mut hashes = Vec::new();
263 for (name, source) in files {
264 let digest = sha2::Sha256::digest(fs_err::read(source)?);
265 hash.update(name.as_bytes());
266 hash.update([0]);
267 hash.update(digest);
268 hashes.push((*name, digest));
269 }
270 let key = hash
271 .finalize()
272 .iter()
273 .map(|byte| format!("{byte:02x}"))
274 .collect::<String>();
275 fs_err::create_dir_all(root)?;
276 let output = root.join(key);
277 if !output.exists() {
278 let scratch = tempfile::Builder::new()
279 .prefix(".prepare-")
280 .tempdir_in(root)?;
281 for (name, source) in files {
282 fs_err::copy(source, scratch.path().join(name))?;
283 }
284 match fs_err::rename(scratch.path(), &output) {
285 Ok(()) => {}
286 Err(error)
287 if matches!(
288 error.kind(),
289 std::io::ErrorKind::AlreadyExists | std::io::ErrorKind::DirectoryNotEmpty
290 ) =>
291 {
292 log::debug!("another writer staged runtime {}", output.display());
293 }
294 Err(error) => return Err(error.into()),
295 }
296 }
297 for (name, digest) in hashes {
298 let path = output.join(name);
299 anyhow::ensure!(
300 fs_err::symlink_metadata(&path)?.file_type().is_file()
301 && sha2::Sha256::digest(fs_err::read(&path)?) == digest,
302 "staged Incubator runtime was modified: {}",
303 path.display()
304 );
305 }
306 Ok(output)
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use test_with_tracing::test;
313
314 const SOURCE_PROFILE: &str =
315 include_str!("../../../../petri/incubator/profiles/x86_64-mshv.toml");
316
317 #[test]
318 fn unsupported_profiles_are_rejected_before_provisioning() {
319 for source in [
320 SOURCE_PROFILE.replace("type = \"openvmm\"", "type = \"qemu-tcg\""),
321 SOURCE_PROFILE.replace("arch = \"x86-64\"", "arch = \"aarch64\""),
322 SOURCE_PROFILE.replace("type = \"mshv\"", "type = \"linux-direct\""),
323 "[incubator]\ntype = 'openvmm'\narch = 'x86-64'\n".into(),
324 ] {
325 assert!(parse_source_profile(&source).is_err());
326 }
327 }
328
329 #[test]
330 fn source_settings_survive_path_resolution_and_explicit_overrides() -> anyhow::Result<()> {
331 let source = SOURCE_PROFILE
332 .replace("memory = \"8G\"", "memory = \"12G\"")
333 .replace("processors = 2", "processors = 4");
334 let paths = [
335 Path::new("/resolved/openvmm"),
336 Path::new("/resolved/host-boot"),
337 Path::new("/resolved/firmware"),
338 Path::new("/resolved/artifacts"),
339 ];
340 let resolved =
341 resolve_profile(&source, paths[0], paths[1], paths[2], paths[3], None, None)?;
342 let config = parse_source_profile(&resolved)?;
343 assert_eq!(config["incubator"]["memory"].as_str(), Some("12G"));
344 assert_eq!(config["incubator"]["processors"].as_integer(), Some(4));
345 assert_eq!(
346 config["incubator"]["binary"].as_str(),
347 Some("/resolved/openvmm")
348 );
349 assert_eq!(
350 config["boot"]["host-boot"].as_str(),
351 Some("/resolved/host-boot")
352 );
353 assert_eq!(
354 config["boot"]["firmware"].as_str(),
355 Some("/resolved/firmware")
356 );
357 assert_eq!(
358 config["boot"]["boot-artifacts"].as_str(),
359 Some("/resolved/artifacts")
360 );
361 let original = parse_source_profile(&source)?;
362 assert_eq!(
363 config["boot"]["kernel-cmdline"].as_str(),
364 original["boot"]["kernel-cmdline"].as_str()
365 );
366 assert_eq!(
367 config["boot"]["hypervisor-cmdline"].as_str(),
368 original["boot"]["hypervisor-cmdline"].as_str()
369 );
370 let overridden = resolve_profile(
371 &source,
372 paths[0],
373 paths[1],
374 paths[2],
375 paths[3],
376 Some("debug"),
377 Some("MSHV_ENABLE=TRUE"),
378 )?;
379 let config = parse_source_profile(&overridden)?;
380 assert_eq!(config["boot"]["kernel-cmdline"].as_str(), Some("debug"));
381 assert_eq!(
382 config["boot"]["hypervisor-cmdline"].as_str(),
383 Some("MSHV_ENABLE=TRUE")
384 );
385 Ok(())
386 }
387
388 #[test]
389 fn retains_coherent_runtime_snapshots_across_builds() -> anyhow::Result<()> {
390 let scratch = tempfile::tempdir()?;
391 let source = scratch.path().join("ephemeral-build");
392 let cache = scratch.path().join("runtime");
393 fs_err::write(&source, b"old build")?;
394 let old = stage_runtime(&cache, &[("incubator", &source)])?;
395 assert_eq!(stage_runtime(&cache, &[("incubator", &source)])?, old);
396 fs_err::write(&source, b"new build")?;
397 let new = stage_runtime(&cache, &[("incubator", &source)])?;
398 assert_ne!(new, old);
399 fs_err::remove_file(&source)?;
400 assert_eq!(fs_err::read(old.join("incubator"))?, b"old build");
401 assert_eq!(fs_err::read(new.join("incubator"))?, b"new build");
402 fs_err::write(&source, b"new build")?;
403 fs_err::write(new.join("incubator"), b"corrupt")?;
404 assert!(stage_runtime(&cache, &[("incubator", &source)]).is_err());
405 Ok(())
406 }
407}