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