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