1use 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
46fn 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 pub incubator: ReadVar<IncubatorOutput>,
68 pub incubator_profile: IncubatorProfileNameOrPath,
70 pub kernel: Option<ReadVar<PathBuf>>,
72 pub initrd: Option<ReadVar<PathBuf>>,
74 pub repo_root: ReadVar<PathBuf>,
79 pub test_content_dir: ReadVar<PathBuf>,
81 pub extra_share_paths: Vec<ReadVar<PathBuf>>,
83 pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
86 pub qemu_binary: Option<ReadVar<PathBuf>>,
88 pub target: target_lexicon::Triple,
91 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
189struct 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
202fn 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 env.insert("INCUBATOR_MAP_COMMAND_PATH".into(), "true".into());
236 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}