Skip to main content

flowey_lib_hvlite/
write_incubator_target_runner.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Compute the environment that runs cargo-nextest tests in an incubator.
5//!
6//! Rather than generating a wrapper script, the incubator binary is itself used
7//! as the cargo-nextest target runner, and all per-run configuration is plumbed
8//! in via `INCUBATOR_*` environment variables (see the `incubator` crate's CLI,
9//! whose options each have a matching `env =` fallback).
10
11use crate::build_incubator::IncubatorOutput;
12use crate::build_incubator::IncubatorProfileNameOrPath;
13use flowey::node::prelude::*;
14use std::collections::BTreeMap;
15use std::path::Path;
16
17const INCUBATOR_ENV_POLICY: &[&str] = &[
18    "RUST_LOG",
19    "RUST_BACKTRACE",
20    "OPENVMM_LOG",
21    "OPENVMM_SHOW_SPANS",
22    "OPENVMM_LOG_SPANS",
23    "PETRI_REMOTE_ARTIFACTS",
24    "PETRI_REUSE_PREPPED_VHDS",
25    "PETRI_IGNORE_UNSTABLE_FAILURES",
26    "OPENVMM_REQUIRE_2MB_HUGETLB",
27    "VMM_TESTS_CONTENT_DIR/p",
28    "TEST_OUTPUT_PATH/p",
29    "VMM_TEST_IMAGES/p",
30    "NEXTEST_WORKSPACE_ROOT/p",
31    "CARGO_MANIFEST_DIR/p",
32    "CARGO_BIN_EXE_*/p",
33    "NEXTEST_BIN_EXE_*/p",
34];
35
36const NEXTEST_ARCHIVE_TMP_DIR: &str = "nextest-archive-tmp";
37const DEFAULT_INCUBATOR_RUST_LOG: &str = "info";
38
39fn cargo_target_runner_env_var(target: &target_lexicon::Triple) -> String {
40    format!(
41        "CARGO_TARGET_{}_RUNNER",
42        target.to_string().replace('-', "_").to_ascii_uppercase()
43    )
44}
45
46/// Merge the policy/runtime environment that xflowey owns into `env`: the cargo
47/// target-runner pointer (the incubator binary itself), a default `RUST_LOG`,
48/// and the `INCUBATOR_ENV` forwarding policy.
49fn add_incubator_target_runner_env(
50    env: &mut BTreeMap<String, String>,
51    target: &target_lexicon::Triple,
52    runner_bin: &Path,
53) {
54    env.insert(
55        cargo_target_runner_env_var(target),
56        runner_bin.display().to_string(),
57    );
58    env.entry("RUST_LOG".into()).or_insert_with(|| {
59        std::env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_INCUBATOR_RUST_LOG.into())
60    });
61    env.insert("INCUBATOR_ENV".into(), INCUBATOR_ENV_POLICY.join(":"));
62}
63
64flowey_request! {
65    pub struct Request {
66        /// Path to the incubator binary.
67        pub incubator: ReadVar<IncubatorOutput>,
68        /// Path to the incubator profile TOML file.
69        pub incubator_profile: IncubatorProfileNameOrPath,
70        /// Path to the guest kernel image. If omitted, incubator auto-detects it.
71        pub kernel: Option<ReadVar<PathBuf>>,
72        /// Path to the base initrd. If omitted, incubator auto-detects it.
73        pub initrd: Option<ReadVar<PathBuf>>,
74        /// Path to the OpenVMM repo root. Must contain any repo-relative paths
75        /// referenced by the runner's environment (e.g. `NEXTEST_WORKSPACE_ROOT`,
76        /// `CARGO_MANIFEST_DIR`) so they fall under the computed incubator share
77        /// root and translate correctly into the guest.
78        pub repo_root: ReadVar<PathBuf>,
79        /// Directory containing VMM test runtime artifacts and test outputs.
80        pub test_content_dir: ReadVar<PathBuf>,
81        /// Additional host paths that must be visible in the incubator share.
82        pub extra_share_paths: Vec<ReadVar<PathBuf>>,
83        /// Additional environment variables used to discover path roots that
84        /// must be visible in the incubator share.
85        pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
86        /// Path to the QEMU binary (overrides the profile's binary setting).
87        pub qemu_binary: Option<ReadVar<PathBuf>>,
88        /// The test target triple, used to name the `CARGO_TARGET_*_RUNNER`
89        /// environment variable.
90        pub target: target_lexicon::Triple,
91        /// The complete cargo-nextest environment: the input `extra_env` plus
92        /// the `INCUBATOR_*` configuration, `TMPDIR`, the
93        /// `CARGO_TARGET_*_RUNNER` pointer, and the `INCUBATOR_ENV` policy.
94        pub nextest_env: WriteVar<BTreeMap<String, String>>,
95    }
96}
97
98new_simple_flow_node!(struct Node);
99
100impl SimpleFlowNode for Node {
101    type Request = Request;
102
103    fn imports(_ctx: &mut ImportCtx<'_>) {}
104
105    fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
106        let Request {
107            incubator,
108            incubator_profile,
109            kernel,
110            initrd,
111            repo_root,
112            test_content_dir,
113            extra_share_paths,
114            extra_env,
115            qemu_binary,
116            target,
117            nextest_env,
118        } = request;
119
120        ctx.emit_rust_step("compute incubator target runner env", |ctx| {
121            let incubator = incubator.claim(ctx);
122            let kernel = kernel.claim(ctx);
123            let initrd = initrd.claim(ctx);
124            let repo_root = repo_root.claim(ctx);
125            let test_content_dir: ReadVar<PathBuf, VarClaimed> = test_content_dir.claim(ctx);
126            let extra_share_paths = extra_share_paths.claim(ctx);
127            let extra_env = extra_env.claim(ctx);
128            let qemu_binary = qemu_binary.claim(ctx);
129            let nextest_env = nextest_env.claim(ctx);
130
131            move |rt| {
132                let repo_root = rt.read(repo_root).absolute()?;
133                let incubator_bin = rt.read(incubator).bin.absolute()?;
134                let profile_path = incubator_profile.resolve(&repo_root).absolute()?;
135                let kernel = kernel.map(|v| rt.read(v).absolute()).transpose()?;
136                let initrd = initrd.map(|v| rt.read(v).absolute()).transpose()?;
137                let test_content_dir = rt.read(test_content_dir).absolute()?;
138                let extra_share_paths = rt
139                    .read(extra_share_paths)
140                    .into_iter()
141                    .map(|p| p.absolute().map_err(Into::into))
142                    .collect::<anyhow::Result<Vec<_>>>()?;
143                let extra_env = extra_env.map(|v| rt.read(v)).unwrap_or_default();
144                let qemu_binary = qemu_binary.map(|v| rt.read(v).absolute()).transpose()?;
145
146                let mut share_paths = vec![repo_root.as_path(), test_content_dir.as_path()];
147                share_paths.extend(extra_share_paths.iter().map(|p| p.as_path()));
148                let images_dir = extra_env.get("VMM_TEST_IMAGES").map(PathBuf::from);
149                if let Some(ref images_dir) = images_dir {
150                    share_paths.push(images_dir.as_path());
151                }
152                let share_root = common_ancestor(&share_paths)?;
153
154                let guest_test_content_dir = guest_path(&share_root, &test_content_dir)?;
155                let output_dir = test_content_dir.join("test_results");
156                let tmp_dir = test_content_dir.join(NEXTEST_ARCHIVE_TMP_DIR);
157                fs_err::create_dir_all(&output_dir)?;
158                fs_err::create_dir_all(&tmp_dir)?;
159
160                incubator_bin.make_executable()?;
161                if let Some(qemu_binary) = &qemu_binary {
162                    qemu_binary.make_executable()?;
163                }
164
165                let mut nextest = extra_env;
166                nextest.extend(incubator_runner_env(IncubatorRunnerConfig {
167                    profile_path: &profile_path,
168                    kernel: kernel.as_deref(),
169                    initrd: initrd.as_deref(),
170                    share_root: &share_root,
171                    output_dir: &output_dir,
172                    guest_pipette: &format!("{guest_test_content_dir}/pipette"),
173                    guest_current_dir: &guest_test_content_dir,
174                    qemu_binary: qemu_binary.as_deref(),
175                    tmp_dir: &tmp_dir,
176                }));
177                add_incubator_target_runner_env(&mut nextest, &target, &incubator_bin);
178
179                rt.write(nextest_env, &nextest);
180
181                Ok(())
182            }
183        });
184
185        Ok(())
186    }
187}
188
189/// Inputs to [`incubator_runner_env`].
190struct IncubatorRunnerConfig<'a> {
191    pub profile_path: &'a Path,
192    pub kernel: Option<&'a Path>,
193    pub initrd: Option<&'a Path>,
194    pub share_root: &'a Path,
195    pub output_dir: &'a Path,
196    pub guest_pipette: &'a str,
197    pub guest_current_dir: &'a str,
198    pub qemu_binary: Option<&'a Path>,
199    pub tmp_dir: &'a Path,
200}
201
202/// Build the per-run `INCUBATOR_*` (and `TMPDIR`) environment that configures
203/// the incubator when it runs as a cargo-nextest target runner. Each variable
204/// mirrors an option on the `incubator` CLI.
205fn incubator_runner_env(config: IncubatorRunnerConfig<'_>) -> BTreeMap<String, String> {
206    let IncubatorRunnerConfig {
207        profile_path,
208        kernel,
209        initrd,
210        share_root,
211        output_dir,
212        guest_pipette,
213        guest_current_dir,
214        qemu_binary,
215        tmp_dir,
216    } = config;
217
218    let mut env = BTreeMap::new();
219    env.insert(
220        "INCUBATOR_PROFILE".into(),
221        profile_path.display().to_string(),
222    );
223    env.insert("INCUBATOR_SHARE".into(), share_root.display().to_string());
224    env.insert(
225        "INCUBATOR_OUTPUT_DIR".into(),
226        output_dir.display().to_string(),
227    );
228    env.insert("INCUBATOR_GUEST_PIPETTE".into(), guest_pipette.to_string());
229    env.insert(
230        "INCUBATOR_GUEST_CURRENT_DIR".into(),
231        guest_current_dir.to_string(),
232    );
233    // The runner always receives a host command path that must be translated
234    // into the guest share.
235    env.insert("INCUBATOR_MAP_COMMAND_PATH".into(), "true".into());
236    // Never drive an interactive PTY / raw mode under cargo-nextest; it would
237    // fight nextest's own Ctrl-C handling.
238    env.insert("INCUBATOR_NO_PTY".into(), "true".into());
239    env.insert("TMPDIR".into(), tmp_dir.display().to_string());
240    if let Some(kernel) = kernel {
241        env.insert("INCUBATOR_KERNEL".into(), kernel.display().to_string());
242    }
243    if let Some(initrd) = initrd {
244        env.insert("INCUBATOR_INITRD".into(), initrd.display().to_string());
245    }
246    if let Some(qemu_binary) = qemu_binary {
247        env.insert(
248            "INCUBATOR_QEMU_BINARY".into(),
249            qemu_binary.display().to_string(),
250        );
251    }
252    env
253}
254
255fn guest_path(share_root: &Path, path: &Path) -> anyhow::Result<String> {
256    let relative = path.strip_prefix(share_root).with_context(|| {
257        format!(
258            "{} is not under share root {}",
259            path.display(),
260            share_root.display()
261        )
262    })?;
263
264    if relative.as_os_str().is_empty() {
265        Ok("/share".to_string())
266    } else {
267        Ok(format!("/share/{}", relative.display()))
268    }
269}
270
271fn common_ancestor(paths: &[&Path]) -> anyhow::Result<PathBuf> {
272    let mut candidate = paths
273        .first()
274        .context("no paths for share root")?
275        .to_path_buf();
276
277    loop {
278        if paths.iter().all(|path| path.starts_with(&candidate)) {
279            return Ok(candidate);
280        }
281
282        if !candidate.pop() {
283            anyhow::bail!("paths do not share a common root")
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn maps_guest_share_paths() {
294        assert_eq!(
295            guest_path(Path::new("/tmp/share"), Path::new("/tmp/share/bin/test")).unwrap(),
296            "/share/bin/test"
297        );
298        assert_eq!(
299            guest_path(Path::new("/tmp/share"), Path::new("/tmp/share")).unwrap(),
300            "/share"
301        );
302    }
303
304    #[test]
305    fn builds_incubator_runner_env() {
306        let env = incubator_runner_env(IncubatorRunnerConfig {
307            profile_path: Path::new("/tmp/profiles/aarch64-tcg.toml"),
308            kernel: Some(Path::new("/tmp/kernel Image")),
309            initrd: Some(Path::new("/tmp/initrd.gz")),
310            share_root: Path::new("/tmp/test content"),
311            output_dir: Path::new("/tmp/test content/test_results"),
312            guest_pipette: "/share/pipette",
313            guest_current_dir: "/share",
314            qemu_binary: Some(Path::new("/tmp/qemu/system-aarch64")),
315            tmp_dir: Path::new("/tmp/test content/nextest-archive-tmp"),
316        });
317
318        assert_eq!(
319            env.get("INCUBATOR_PROFILE").unwrap(),
320            "/tmp/profiles/aarch64-tcg.toml"
321        );
322        assert_eq!(env.get("INCUBATOR_KERNEL").unwrap(), "/tmp/kernel Image");
323        assert_eq!(env.get("INCUBATOR_INITRD").unwrap(), "/tmp/initrd.gz");
324        assert_eq!(env.get("INCUBATOR_SHARE").unwrap(), "/tmp/test content");
325        assert_eq!(
326            env.get("INCUBATOR_OUTPUT_DIR").unwrap(),
327            "/tmp/test content/test_results"
328        );
329        assert_eq!(
330            env.get("INCUBATOR_GUEST_PIPETTE").unwrap(),
331            "/share/pipette"
332        );
333        assert_eq!(env.get("INCUBATOR_GUEST_CURRENT_DIR").unwrap(), "/share");
334        assert_eq!(env.get("INCUBATOR_MAP_COMMAND_PATH").unwrap(), "true");
335        assert_eq!(
336            env.get("INCUBATOR_QEMU_BINARY").unwrap(),
337            "/tmp/qemu/system-aarch64"
338        );
339        assert_eq!(
340            env.get("TMPDIR").unwrap(),
341            "/tmp/test content/nextest-archive-tmp"
342        );
343    }
344
345    #[test]
346    fn omits_optional_incubator_env() {
347        let env = incubator_runner_env(IncubatorRunnerConfig {
348            profile_path: Path::new("/tmp/profile.toml"),
349            kernel: None,
350            initrd: None,
351            share_root: Path::new("/tmp/share"),
352            output_dir: Path::new("/tmp/share/test_results"),
353            guest_pipette: "/share/pipette",
354            guest_current_dir: "/share",
355            qemu_binary: None,
356            tmp_dir: Path::new("/tmp/share/nextest-archive-tmp"),
357        });
358
359        assert!(!env.contains_key("INCUBATOR_KERNEL"));
360        assert!(!env.contains_key("INCUBATOR_INITRD"));
361        assert!(!env.contains_key("INCUBATOR_QEMU_BINARY"));
362    }
363
364    #[test]
365    fn builds_cargo_target_runner_env_var() {
366        assert_eq!(
367            cargo_target_runner_env_var(&target_lexicon::triple!("aarch64-unknown-linux-musl")),
368            "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUNNER"
369        );
370    }
371
372    #[test]
373    fn adds_incubator_target_runner_env() {
374        let mut env = BTreeMap::new();
375        let runner = Path::new("tmp").join("incubator");
376        add_incubator_target_runner_env(
377            &mut env,
378            &target_lexicon::triple!("aarch64-unknown-linux-musl"),
379            &runner,
380        );
381
382        assert_eq!(
383            env.get("CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUNNER")
384                .unwrap(),
385            &runner.display().to_string()
386        );
387        assert_eq!(
388            env.get("RUST_LOG").unwrap(),
389            &std::env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_INCUBATOR_RUST_LOG.into())
390        );
391        assert_eq!(
392            env.get("INCUBATOR_ENV").unwrap(),
393            &INCUBATOR_ENV_POLICY.join(":")
394        );
395        assert!(
396            !env.get("INCUBATOR_ENV")
397                .unwrap()
398                .contains("LD_LIBRARY_PATH")
399        );
400    }
401
402    #[test]
403    fn keeps_explicit_incubator_rust_log() {
404        let mut env = BTreeMap::from([("RUST_LOG".into(), "warn,mesh=off".into())]);
405        add_incubator_target_runner_env(
406            &mut env,
407            &target_lexicon::triple!("aarch64-unknown-linux-musl"),
408            Path::new("/tmp/incubator"),
409        );
410
411        assert_eq!(env.get("RUST_LOG").unwrap(), "warn,mesh=off");
412    }
413}