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