1use crate::run_cargo_build::CargoBuildProfile;
7use crate::run_cargo_nextest_run::build_params;
8use flowey::node::prelude::*;
9use std::collections::BTreeMap;
10use std::ffi::OsString;
11
12flowey_request! {
13 pub struct Request {
14 pub run_kind_deps: RunKindDeps,
16 pub working_dir: ReadVar<PathBuf>,
18 pub config_file: ReadVar<PathBuf>,
20 pub tool_config_files: Vec<(String, ReadVar<PathBuf>)>,
22 pub nextest_profile: String,
25 pub nextest_filter_expr: Option<String>,
27 pub run_ignored: bool,
29 pub fail_fast: Option<bool>,
31 pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
33 pub extra_commands: Option<ReadVar<Vec<(OsString, Vec<OsString>)>>>,
35 pub portable: bool,
37 pub command: WriteVar<Script>,
39 }
40}
41
42#[derive(Serialize, Deserialize)]
43pub enum RunKindDeps<C = VarNotClaimed> {
44 BuildAndRun {
45 params: build_params::NextestBuildParams<C>,
46 nextest_installed: ReadVar<SideEffect, C>,
47 rust_toolchain: ReadVar<Option<String>, C>,
48 cargo_flags: ReadVar<crate::cfg_cargo_common_flags::Flags, C>,
49 },
50 RunFromArchive {
51 archive_file: ReadVar<PathBuf, C>,
52 nextest_bin: ReadVar<PathBuf, C>,
53 target: ReadVar<target_lexicon::Triple, C>,
54 },
55}
56
57#[derive(Serialize, Deserialize)]
58pub enum CommandShell {
59 Powershell,
60 Bash,
61}
62
63#[derive(Serialize, Deserialize)]
64pub struct Script {
65 pub env: BTreeMap<String, String>,
66 pub commands: Vec<(OsString, Vec<OsString>)>,
67 pub shell: CommandShell,
68}
69
70new_flow_node!(struct Node);
71
72impl FlowNode for Node {
73 type Request = Request;
74
75 fn imports(ctx: &mut ImportCtx<'_>) {
76 ctx.import::<crate::cfg_cargo_common_flags::Node>();
77 ctx.import::<crate::download_cargo_nextest::Node>();
78 ctx.import::<crate::install_cargo_nextest::Node>();
79 ctx.import::<crate::install_rust::Node>();
80 }
81
82 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
83 for Request {
84 run_kind_deps,
85 working_dir,
86 config_file,
87 tool_config_files,
88 nextest_profile,
89 extra_env,
90 extra_commands,
91 nextest_filter_expr,
92 run_ignored,
93 fail_fast,
94 portable,
95 command,
96 } in requests
97 {
98 ctx.emit_rust_step("generate nextest command", |ctx| {
99 let run_kind_deps = run_kind_deps.claim(ctx);
100 let working_dir = working_dir.claim(ctx);
101 let config_file = config_file.claim(ctx);
102 let tool_config_files = tool_config_files
103 .into_iter()
104 .map(|(a, b)| (a, b.claim(ctx)))
105 .collect::<Vec<_>>();
106 let extra_env = extra_env.claim(ctx);
107 let extra_commands = extra_commands.claim(ctx);
108 let command = command.claim(ctx);
109
110 move |rt| {
111 let working_dir = rt.read(working_dir);
112 let config_file = rt.read(config_file);
113 let mut with_env = rt.read(extra_env).unwrap_or_default();
114 let mut commands = rt.read(extra_commands).unwrap_or_default();
115
116 let target = match &run_kind_deps {
117 RunKindDeps::BuildAndRun {
118 params: build_params::NextestBuildParams { target, .. },
119 ..
120 } => target.clone(),
121 RunKindDeps::RunFromArchive { target, .. } => rt.read(target.clone()),
122 };
123
124 let windows_target = matches!(
125 target.operating_system,
126 target_lexicon::OperatingSystem::Windows
127 );
128 let windows_via_wsl2 = windows_target && crate::_util::running_in_wsl(rt);
129
130 let working_dir_ref = working_dir.as_path();
131 let working_dir_win = windows_via_wsl2.then(|| {
132 crate::_util::wslpath::linux_to_win(working_dir_ref)
133 .display()
134 .to_string()
135 });
136 let maybe_convert_path = |path: PathBuf| -> anyhow::Result<PathBuf> {
137 let path = if windows_via_wsl2 {
138 crate::_util::wslpath::linux_to_win(path)
139 } else {
140 path.absolute()
141 .with_context(|| format!("invalid path {}", path.display()))?
142 };
143 let path = if portable {
144 if windows_target {
145 let working_dir_trimmed =
146 working_dir_win.as_ref().unwrap().trim_end_matches('\\');
147 let path_win = path.display().to_string();
148 let path_trimmed = path_win.trim_end_matches('\\');
149 PathBuf::from(format!(
150 "$PSScriptRoot{}",
151 path_trimmed
152 .strip_prefix(working_dir_trimmed)
153 .with_context(|| format!(
154 "{} not in {}",
155 path_win, working_dir_trimmed
156 ),)?
157 ))
158 } else {
159 path.strip_prefix(working_dir_ref)
160 .with_context(|| {
161 format!(
162 "{} not in {}",
163 path.display(),
164 working_dir_ref.display()
165 )
166 })?
167 .to_path_buf()
168 }
169 } else {
170 path
171 };
172 Ok(path)
173 };
174
175 enum NextestInvocation {
176 Standalone { nextest_bin: PathBuf },
178 WithCargo { rust_toolchain: Option<String> },
180 }
181
182 let (nextest_invocation, build_args, build_env) = match run_kind_deps {
188 RunKindDeps::BuildAndRun {
189 params:
190 build_params::NextestBuildParams {
191 packages,
192 features,
193 no_default_features,
194 unstable_panic_abort_tests,
195 target,
196 profile,
197 extra_env,
198 },
199 nextest_installed: _, rust_toolchain,
201 cargo_flags,
202 } => {
203 let (mut build_args, build_env) = cargo_nextest_build_args_and_env(
204 rt.read(cargo_flags),
205 profile,
206 target,
207 rt.read(packages),
208 features,
209 unstable_panic_abort_tests,
210 no_default_features,
211 rt.read(extra_env),
212 );
213
214 let nextest_invocation = NextestInvocation::WithCargo {
215 rust_toolchain: rt.read(rust_toolchain),
216 };
217
218 let cargo_metadata_path = std::env::current_dir()?
222 .absolute()?
223 .join("cargo_metadata.json");
224
225 let sh = xshell::Shell::new()?;
226 sh.change_dir(&working_dir);
227 let output =
228 xshell::cmd!(sh, "cargo metadata --format-version 1").output()?;
229 let cargo_metadata = String::from_utf8(output.stdout)?;
230 fs_err::write(&cargo_metadata_path, cargo_metadata)?;
231
232 build_args.push("--cargo-metadata".into());
233 build_args.push(cargo_metadata_path.display().to_string());
234
235 (nextest_invocation, build_args, build_env)
236 }
237 RunKindDeps::RunFromArchive {
238 archive_file,
239 nextest_bin,
240 target: _,
241 } => {
242 let build_args = vec![
243 "--archive-file".into(),
244 maybe_convert_path(rt.read(archive_file))?
245 .display()
246 .to_string(),
247 ];
248
249 let nextest_invocation = NextestInvocation::Standalone {
250 nextest_bin: rt.read(nextest_bin),
251 };
252
253 (nextest_invocation, build_args, BTreeMap::default())
254 }
255 };
256
257 let mut args: Vec<OsString> = Vec::new();
258
259 let argv0: OsString = match nextest_invocation {
260 NextestInvocation::Standalone { nextest_bin } => if portable {
261 maybe_convert_path(nextest_bin)?
262 } else {
263 nextest_bin
264 }
265 .into(),
266 NextestInvocation::WithCargo { rust_toolchain } => {
267 if let Some(rust_toolchain) = rust_toolchain {
268 args.extend(["run".into(), rust_toolchain.into(), "cargo".into()]);
269 "rustup".into()
270 } else {
271 "cargo".into()
272 }
273 }
274 };
275
276 args.extend([
277 "nextest".into(),
278 "run".into(),
279 "--profile".into(),
280 (&nextest_profile).into(),
281 "--config-file".into(),
282 maybe_convert_path(config_file)?.into(),
283 "--workspace-remap".into(),
284 maybe_convert_path(working_dir.clone())?.into(),
285 ]);
286
287 for (tool, config_file) in tool_config_files {
288 args.extend([
289 "--tool-config-file".into(),
290 format!(
291 "{}:{}",
292 tool,
293 maybe_convert_path(rt.read(config_file))?.display()
294 )
295 .into(),
296 ]);
297 }
298
299 args.extend(build_args.into_iter().map(Into::into));
300
301 if let Some(nextest_filter_expr) = nextest_filter_expr {
302 args.push("--filter-expr".into());
303 args.push(nextest_filter_expr.into());
304 }
305
306 if run_ignored {
307 args.push("--run-ignored".into());
308 args.push("all".into());
309 }
310
311 if let Some(fail_fast) = fail_fast {
312 if fail_fast {
313 args.push("--fail-fast".into());
314 } else {
315 args.push("--no-fail-fast".into());
316 }
317 }
318
319 if !with_env.contains_key("RUST_BACKTRACE") {
321 with_env.insert("RUST_BACKTRACE".into(), "1".into());
322 }
323
324 if !matches!(rt.backend(), FlowBackend::Local) {
327 with_env.insert("CARGO_INCREMENTAL".into(), "0".into());
328 }
329
330 if !portable && crate::_util::running_in_wsl(rt) {
332 let old_wslenv = std::env::var("WSLENV");
333 let new_wslenv = with_env.keys().cloned().collect::<Vec<_>>().join(":");
334 with_env.insert(
335 "WSLENV".into(),
336 format!(
337 "{}{}",
338 old_wslenv.map(|s| s + ":").unwrap_or_default(),
339 new_wslenv
340 ),
341 );
342 }
343
344 with_env.extend(build_env);
348
349 commands.push((argv0, args));
350
351 rt.write(
352 command,
353 &Script {
354 env: with_env,
355 commands,
356 shell: if (portable || !windows_via_wsl2)
357 && matches!(
358 target.operating_system,
359 target_lexicon::OperatingSystem::Windows
360 ) {
361 CommandShell::Powershell
362 } else {
363 CommandShell::Bash
364 },
365 },
366 );
367
368 Ok(())
369 }
370 });
371 }
372
373 Ok(())
374 }
375}
376
377pub(crate) fn cargo_nextest_build_args_and_env(
379 cargo_flags: crate::cfg_cargo_common_flags::Flags,
380 cargo_profile: CargoBuildProfile,
381 target: target_lexicon::Triple,
382 packages: build_params::TestPackages,
383 features: crate::run_cargo_build::CargoFeatureSet,
384 unstable_panic_abort_tests: Option<build_params::PanicAbortTests>,
385 no_default_features: bool,
386 mut extra_env: BTreeMap<String, String>,
387) -> (Vec<String>, BTreeMap<String, String>) {
388 let locked = cargo_flags.locked.then_some("--locked");
389 let verbose = cargo_flags.verbose.then_some("--verbose");
390 let cargo_profile = match &cargo_profile {
391 CargoBuildProfile::Debug => "dev",
392 CargoBuildProfile::Release => "release",
393 CargoBuildProfile::Custom(s) => s,
394 };
395 let target = target.to_string();
396
397 let packages: Vec<String> = {
398 let mut v = vec!["--tests".into(), "--bins".into()];
400
401 match packages {
402 build_params::TestPackages::Workspace { exclude } => {
403 v.push("--workspace".into());
404 for crate_name in exclude {
405 v.push("--exclude".into());
406 v.push(crate_name);
407 }
408 }
409 build_params::TestPackages::Crates { crates } => {
410 for crate_name in crates {
411 v.push("-p".into());
412 v.push(crate_name);
413 }
414 }
415 }
416
417 v
418 };
419
420 let (z_panic_abort_tests, use_rustc_bootstrap) = match unstable_panic_abort_tests {
421 Some(kind) => (
422 Some("-Zpanic-abort-tests"),
423 match kind {
424 build_params::PanicAbortTests::UsingNightly => false,
425 build_params::PanicAbortTests::UsingRustcBootstrap => true,
426 },
427 ),
428 None => (None, false),
429 };
430
431 let mut args = Vec::new();
432 args.extend(locked.map(Into::into));
433 args.extend(verbose.map(Into::into));
434 args.push("--cargo-profile".into());
435 args.push(cargo_profile.into());
436 args.extend(z_panic_abort_tests.map(Into::into));
437 args.push("--target".into());
438 args.push(target);
439 args.extend(packages);
440 if no_default_features {
441 args.push("--no-default-features".into())
442 }
443 args.extend(features.to_cargo_arg_strings());
444
445 let mut env = BTreeMap::new();
446 if use_rustc_bootstrap {
447 env.insert("RUSTC_BOOTSTRAP".into(), "1".into());
448 }
449 env.append(&mut extra_env);
450
451 (args, env)
452}
453
454impl RunKindDeps {
456 pub fn claim(self, ctx: &mut StepCtx<'_>) -> RunKindDeps<VarClaimed> {
457 match self {
458 RunKindDeps::BuildAndRun {
459 params,
460 nextest_installed,
461 rust_toolchain,
462 cargo_flags,
463 } => RunKindDeps::BuildAndRun {
464 params: params.claim(ctx),
465 nextest_installed: nextest_installed.claim(ctx),
466 rust_toolchain: rust_toolchain.claim(ctx),
467 cargo_flags: cargo_flags.claim(ctx),
468 },
469 RunKindDeps::RunFromArchive {
470 archive_file,
471 nextest_bin,
472 target,
473 } => RunKindDeps::RunFromArchive {
474 archive_file: archive_file.claim(ctx),
475 nextest_bin: nextest_bin.claim(ctx),
476 target: target.claim(ctx),
477 },
478 }
479 }
480}
481
482impl std::fmt::Display for Script {
483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484 let quote_char = match self.shell {
485 CommandShell::Powershell => "\"",
486 CommandShell::Bash => "'",
487 };
488
489 let env_string = match self.shell {
490 CommandShell::Powershell => self
491 .env
492 .iter()
493 .map(|(k, v)| format!("$env:{k}=\"{v}\""))
494 .collect::<Vec<_>>()
495 .join("\n"),
496 CommandShell::Bash => self
497 .env
498 .iter()
499 .map(|(k, v)| format!("export {k}=\"{v}\""))
500 .collect::<Vec<_>>()
501 .join("\n"),
502 };
503 writeln!(f, "{env_string}")?;
504
505 for cmd in &self.commands {
506 let argv0_string = cmd.0.to_string_lossy();
507 let argv0_string = match self.shell {
508 CommandShell::Powershell => format!("&\"{argv0_string}\""),
509 CommandShell::Bash => format!("\"{argv0_string}\""),
510 };
511
512 let arg_string = {
513 cmd.1
514 .iter()
515 .map(|v| format!("{quote_char}{}{quote_char}", v.to_string_lossy()))
516 .collect::<Vec<_>>()
517 .join(" ")
518 };
519 writeln!(f, "{argv0_string} {arg_string}")?;
520 }
521
522 Ok(())
523 }
524}