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(rt, working_dir_ref)
133 .display()
134 .to_string()
135 });
136
137 let tool_config_files: Vec<(String, PathBuf)> = tool_config_files
138 .into_iter()
139 .map(|(tool, var)| (tool, rt.read(var)))
140 .collect();
141
142 enum NextestInvocation {
143 Standalone { nextest_bin: PathBuf },
145 WithCargo { rust_toolchain: Option<String> },
147 }
148
149 let (nextest_invocation, build_args, archive_file, build_env) =
155 match run_kind_deps {
156 RunKindDeps::BuildAndRun {
157 params:
158 build_params::NextestBuildParams {
159 packages,
160 features,
161 no_default_features,
162 target,
163 profile,
164 extra_env,
165 },
166 nextest_installed: _, rust_toolchain,
168 cargo_flags,
169 } => {
170 let (mut build_args, build_env) = cargo_nextest_build_args_and_env(
171 rt.read(cargo_flags),
172 profile,
173 target,
174 rt.read(packages),
175 features,
176 no_default_features,
177 rt.read(extra_env),
178 );
179
180 let nextest_invocation = NextestInvocation::WithCargo {
181 rust_toolchain: rt.read(rust_toolchain),
182 };
183
184 let cargo_metadata_path = std::env::current_dir()?
188 .absolute()?
189 .join("cargo_metadata.json");
190
191 rt.sh.change_dir(&working_dir);
192 let output =
193 flowey::shell_cmd!(rt, "cargo metadata --format-version 1")
194 .output()?;
195 let cargo_metadata = String::from_utf8(output.stdout)?;
196 fs_err::write(&cargo_metadata_path, cargo_metadata)?;
197
198 build_args.push("--cargo-metadata".into());
199 build_args.push(cargo_metadata_path.display().to_string());
200
201 (nextest_invocation, build_args, None, build_env)
202 }
203 RunKindDeps::RunFromArchive {
204 archive_file,
205 nextest_bin,
206 target: _,
207 } => {
208 let archive_file = rt.read(archive_file);
209 let nextest_bin = rt.read(nextest_bin);
210
211 (
212 NextestInvocation::Standalone { nextest_bin },
213 vec![],
214 Some(archive_file),
215 BTreeMap::default(),
216 )
217 }
218 };
219
220 let wsl_convert_path = |path: PathBuf| -> anyhow::Result<PathBuf> {
223 if windows_via_wsl2 {
224 Ok(crate::_util::wslpath::linux_to_win(rt, path))
225 } else {
226 path.absolute()
227 .with_context(|| format!("invalid path {}", path.display()))
228 }
229 };
230
231 let config_file = wsl_convert_path(config_file)?;
234 let converted_working_dir = wsl_convert_path(working_dir.clone())?;
235 let tool_config_files: Vec<(String, PathBuf)> = tool_config_files
236 .into_iter()
237 .map(|(tool, path)| Ok((tool, wsl_convert_path(path)?)))
238 .collect::<anyhow::Result<_>>()?;
239 let archive_file = archive_file.map(&wsl_convert_path).transpose()?;
240
241 let make_portable_path = |path: PathBuf| -> anyhow::Result<PathBuf> {
243 let path = if portable {
244 if windows_target {
245 let working_dir_trimmed =
246 working_dir_win.as_ref().unwrap().trim_end_matches('\\');
247 let path_win = path.display().to_string();
248 let path_trimmed = path_win.trim_end_matches('\\');
249 PathBuf::from(format!(
250 "$PSScriptRoot{}",
251 path_trimmed
252 .strip_prefix(working_dir_trimmed)
253 .with_context(|| format!(
254 "{} not in {}",
255 path_win, working_dir_trimmed
256 ),)?
257 ))
258 } else {
259 path.strip_prefix(working_dir_ref)
260 .with_context(|| {
261 format!(
262 "{} not in {}",
263 path.display(),
264 working_dir_ref.display()
265 )
266 })?
267 .to_path_buf()
268 }
269 } else {
270 path
271 };
272 Ok(path)
273 };
274
275 let mut args: Vec<OsString> = Vec::new();
276
277 let argv0: OsString = match nextest_invocation {
278 NextestInvocation::Standalone { nextest_bin } => if portable {
279 make_portable_path(wsl_convert_path(nextest_bin)?)?
280 } else {
281 nextest_bin
282 }
283 .into(),
284 NextestInvocation::WithCargo { rust_toolchain } => {
285 if let Some(rust_toolchain) = rust_toolchain {
286 args.extend(["run".into(), rust_toolchain.into(), "cargo".into()]);
287 "rustup".into()
288 } else {
289 "cargo".into()
290 }
291 }
292 };
293
294 args.extend([
295 "nextest".into(),
296 "run".into(),
297 "--profile".into(),
298 (&nextest_profile).into(),
299 "--config-file".into(),
300 make_portable_path(config_file)?.into(),
301 "--workspace-remap".into(),
302 make_portable_path(converted_working_dir)?.into(),
303 ]);
304
305 for (tool, config_file) in tool_config_files {
306 args.extend([
307 "--tool-config-file".into(),
308 format!("{}:{}", tool, make_portable_path(config_file)?.display())
309 .into(),
310 ]);
311 }
312
313 if let Some(archive_file) = archive_file {
314 args.extend([
315 "--archive-file".into(),
316 make_portable_path(archive_file)?.into(),
317 ]);
318 }
319
320 args.extend(build_args.into_iter().map(Into::into));
321
322 if let Some(nextest_filter_expr) = nextest_filter_expr {
323 args.push("--filter-expr".into());
324 args.push(nextest_filter_expr.into());
325 }
326
327 if run_ignored {
328 args.push("--run-ignored".into());
329 args.push("all".into());
330 }
331
332 if let Some(fail_fast) = fail_fast {
333 if fail_fast {
334 args.push("--fail-fast".into());
335 } else {
336 args.push("--no-fail-fast".into());
337 }
338 }
339
340 if !with_env.contains_key("RUST_BACKTRACE") {
342 with_env.insert("RUST_BACKTRACE".into(), "1".into());
343 }
344
345 if !matches!(rt.backend(), FlowBackend::Local) {
348 with_env.insert("CARGO_INCREMENTAL".into(), "0".into());
349 }
350
351 if !portable && crate::_util::running_in_wsl(rt) {
353 let old_wslenv = std::env::var("WSLENV");
354 let new_wslenv = with_env.keys().cloned().collect::<Vec<_>>().join(":");
355 with_env.insert(
356 "WSLENV".into(),
357 format!(
358 "{}{}",
359 old_wslenv.map(|s| s + ":").unwrap_or_default(),
360 new_wslenv
361 ),
362 );
363 }
364
365 with_env.extend(build_env);
369
370 commands.push((argv0, args));
371
372 rt.write(
373 command,
374 &Script {
375 env: with_env,
376 commands,
377 shell: if (portable || !windows_via_wsl2)
378 && matches!(
379 target.operating_system,
380 target_lexicon::OperatingSystem::Windows
381 ) {
382 CommandShell::Powershell
383 } else {
384 CommandShell::Bash
385 },
386 },
387 );
388
389 Ok(())
390 }
391 });
392 }
393
394 Ok(())
395 }
396}
397
398pub(crate) fn cargo_nextest_build_args_and_env(
400 cargo_flags: crate::cfg_cargo_common_flags::Flags,
401 cargo_profile: CargoBuildProfile,
402 target: target_lexicon::Triple,
403 packages: build_params::TestPackages,
404 features: crate::run_cargo_build::CargoFeatureSet,
405 no_default_features: bool,
406 mut extra_env: BTreeMap<String, String>,
407) -> (Vec<String>, BTreeMap<String, String>) {
408 let locked = cargo_flags.locked.then_some("--locked");
409 let verbose = cargo_flags.verbose.then_some("--verbose");
410 let cargo_profile = match &cargo_profile {
411 CargoBuildProfile::Debug => "dev",
412 CargoBuildProfile::Release => "release",
413 CargoBuildProfile::Custom(s) => s,
414 };
415 let target = target.to_string();
416
417 let packages: Vec<String> = {
418 let mut v = vec!["--tests".into(), "--bins".into()];
420
421 match packages {
422 build_params::TestPackages::Workspace { exclude } => {
423 v.push("--workspace".into());
424 for crate_name in exclude {
425 v.push("--exclude".into());
426 v.push(crate_name);
427 }
428 }
429 build_params::TestPackages::Crates { crates } => {
430 for crate_name in crates {
431 v.push("-p".into());
432 v.push(crate_name);
433 }
434 }
435 }
436
437 v
438 };
439
440 let mut args = Vec::new();
441 args.extend(locked.map(Into::into));
442 args.extend(verbose.map(Into::into));
443 args.push("--cargo-profile".into());
444 args.push(cargo_profile.into());
445 args.push("--target".into());
446 args.push(target);
447 args.extend(packages);
448 if no_default_features {
449 args.push("--no-default-features".into())
450 }
451 args.extend(features.to_cargo_arg_strings());
452
453 let mut env = BTreeMap::new();
454
455 env.append(&mut extra_env);
456
457 (args, env)
458}
459
460impl RunKindDeps {
462 pub fn claim(self, ctx: &mut StepCtx<'_>) -> RunKindDeps<VarClaimed> {
463 match self {
464 RunKindDeps::BuildAndRun {
465 params,
466 nextest_installed,
467 rust_toolchain,
468 cargo_flags,
469 } => RunKindDeps::BuildAndRun {
470 params: params.claim(ctx),
471 nextest_installed: nextest_installed.claim(ctx),
472 rust_toolchain: rust_toolchain.claim(ctx),
473 cargo_flags: cargo_flags.claim(ctx),
474 },
475 RunKindDeps::RunFromArchive {
476 archive_file,
477 nextest_bin,
478 target,
479 } => RunKindDeps::RunFromArchive {
480 archive_file: archive_file.claim(ctx),
481 nextest_bin: nextest_bin.claim(ctx),
482 target: target.claim(ctx),
483 },
484 }
485 }
486}
487
488impl std::fmt::Display for Script {
489 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490 let quote_char = match self.shell {
491 CommandShell::Powershell => "\"",
492 CommandShell::Bash => "'",
493 };
494
495 let env_string = match self.shell {
496 CommandShell::Powershell => self
497 .env
498 .iter()
499 .map(|(k, v)| format!("$env:{k}=\"{v}\""))
500 .collect::<Vec<_>>()
501 .join("\n"),
502 CommandShell::Bash => self
503 .env
504 .iter()
505 .map(|(k, v)| format!("export {k}=\"{v}\""))
506 .collect::<Vec<_>>()
507 .join("\n"),
508 };
509 writeln!(f, "{env_string}")?;
510
511 for cmd in &self.commands {
512 let argv0_string = cmd.0.to_string_lossy();
513 let argv0_string = match self.shell {
514 CommandShell::Powershell => format!("&\"{argv0_string}\""),
515 CommandShell::Bash => format!("\"{argv0_string}\""),
516 };
517
518 let arg_string = {
519 cmd.1
520 .iter()
521 .map(|v| format!("{quote_char}{}{quote_char}", v.to_string_lossy()))
522 .collect::<Vec<_>>()
523 .join(" ")
524 };
525 writeln!(f, "{argv0_string} {arg_string}")?;
526 }
527
528 Ok(())
529 }
530}