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