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: crate::run_cargo_build::CargoFeatureSet,
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 (z_panic_abort_tests, use_rustc_bootstrap) = match unstable_panic_abort_tests {
416 Some(kind) => (
417 Some("-Zpanic-abort-tests"),
418 match kind {
419 build_params::PanicAbortTests::UsingNightly => false,
420 build_params::PanicAbortTests::UsingRustcBootstrap => true,
421 },
422 ),
423 None => (None, false),
424 };
425
426 let mut args = Vec::new();
427 args.extend(locked.map(Into::into));
428 args.extend(verbose.map(Into::into));
429 args.push("--cargo-profile".into());
430 args.push(cargo_profile.into());
431 args.extend(z_panic_abort_tests.map(Into::into));
432 args.push("--target".into());
433 args.push(target);
434 args.extend(packages);
435 if no_default_features {
436 args.push("--no-default-features".into())
437 }
438 args.extend(features.to_cargo_arg_strings());
439
440 let mut env = BTreeMap::new();
441 if use_rustc_bootstrap {
442 env.insert("RUSTC_BOOTSTRAP".into(), "1".into());
443 }
444 env.append(&mut extra_env);
445
446 (args, env)
447}
448
449impl RunKindDeps {
451 pub fn claim(self, ctx: &mut StepCtx<'_>) -> RunKindDeps<VarClaimed> {
452 match self {
453 RunKindDeps::BuildAndRun {
454 params,
455 nextest_installed,
456 rust_toolchain,
457 cargo_flags,
458 } => RunKindDeps::BuildAndRun {
459 params: params.claim(ctx),
460 nextest_installed: nextest_installed.claim(ctx),
461 rust_toolchain: rust_toolchain.claim(ctx),
462 cargo_flags: cargo_flags.claim(ctx),
463 },
464 RunKindDeps::RunFromArchive {
465 archive_file,
466 nextest_bin,
467 target,
468 } => RunKindDeps::RunFromArchive {
469 archive_file: archive_file.claim(ctx),
470 nextest_bin: nextest_bin.claim(ctx),
471 target: target.claim(ctx),
472 },
473 }
474 }
475}
476
477impl std::fmt::Display for Command {
478 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479 let quote_char = match self.shell {
480 CommandShell::Powershell => "\"",
481 CommandShell::Bash => "'",
482 };
483 let arg_string = {
484 self.args
485 .iter()
486 .map(|v| format!("{quote_char}{}{quote_char}", v.to_string_lossy()))
487 .collect::<Vec<_>>()
488 .join(" ")
489 };
490
491 let env_string = match self.shell {
492 CommandShell::Powershell => self
493 .env
494 .iter()
495 .map(|(k, v)| format!("$env:{k}=\"{v}\";"))
496 .collect::<Vec<_>>()
497 .join(" "),
498 CommandShell::Bash => self
499 .env
500 .iter()
501 .map(|(k, v)| format!("{k}=\"{v}\""))
502 .collect::<Vec<_>>()
503 .join(" "),
504 };
505
506 let argv0_string = self.argv0.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 write!(f, "{} {} {}", env_string, argv0_string, arg_string)
513 }
514}