diff options
Diffstat (limited to 'xtask/src')
| -rw-r--r-- | xtask/src/argument_parsing.rs | 85 | ||||
| -rw-r--r-- | xtask/src/cargo_command.rs | 750 | ||||
| -rw-r--r-- | xtask/src/cargo_commands.rs | 345 | ||||
| -rw-r--r-- | xtask/src/command.rs | 779 | ||||
| -rw-r--r-- | xtask/src/main.rs | 156 | ||||
| -rw-r--r-- | xtask/src/run.rs | 501 | ||||
| -rw-r--r-- | xtask/src/run/data.rs | 87 | ||||
| -rw-r--r-- | xtask/src/run/iter.rs | 48 | ||||
| -rw-r--r-- | xtask/src/run/results.rs | 100 |
9 files changed, 1580 insertions, 1271 deletions
diff --git a/xtask/src/argument_parsing.rs b/xtask/src/argument_parsing.rs index 3ee9e34..05d0ae4 100644 --- a/xtask/src/argument_parsing.rs +++ b/xtask/src/argument_parsing.rs @@ -1,4 +1,4 @@ -use crate::{command::CargoCommand, Target, ARMV6M, ARMV7M, ARMV8MBASE, ARMV8MMAIN}; +use crate::{cargo_command::CargoCommand, Target, ARMV6M, ARMV7M, ARMV8MBASE, ARMV8MMAIN}; use clap::{Args, Parser, Subcommand}; use core::fmt; @@ -19,15 +19,17 @@ impl fmt::Display for Package { } impl Package { - pub fn name(&self) -> &str { - match self { + pub fn name(&self) -> String { + let name = match self { Package::Rtic => "rtic", Package::RticCommon => "rtic-common", Package::RticMacros => "rtic-macros", Package::RticMonotonics => "rtic-monotonics", Package::RticSync => "rtic-sync", Package::RticTime => "rtic-time", - } + }; + + name.to_string() } pub fn all() -> Vec<Self> { @@ -102,35 +104,41 @@ impl TestMetadata { ); let features = Some(backend.to_target().and_features(&features)); CargoCommand::Test { - package: Some(package), + package: Some(package.name()), features, test: Some("ui".to_owned()), + deny_warnings: true, } } Package::RticMacros => CargoCommand::Test { - package: Some(package), + package: Some(package.name()), features: Some(backend.to_rtic_macros_feature().to_owned()), test: None, + deny_warnings: true, }, Package::RticSync => CargoCommand::Test { - package: Some(package), + package: Some(package.name()), features: Some("testing".to_owned()), test: None, + deny_warnings: true, }, Package::RticCommon => CargoCommand::Test { - package: Some(package), + package: Some(package.name()), features: Some("testing".to_owned()), test: None, + deny_warnings: true, }, Package::RticMonotonics => CargoCommand::Test { - package: Some(package), + package: Some(package.name()), features: None, test: None, + deny_warnings: true, }, Package::RticTime => CargoCommand::Test { - package: Some(package), + package: Some(package.name()), features: Some("critical-section/std".into()), test: None, + deny_warnings: true, }, } } @@ -190,8 +198,12 @@ pub enum BuildOrCheck { #[derive(Parser, Clone)] pub struct Globals { - /// For which backend to build (defaults to thumbv7) - #[arg(value_enum, short, long, global = true)] + /// Error out on warnings + #[arg(short = 'D', long)] + pub deny_warnings: bool, + + /// For which backend to build. + #[arg(value_enum, short, default_value = "thumbv7", long, global = true)] pub backend: Option<Backends>, /// List of comma separated examples to include, all others are excluded @@ -300,6 +312,55 @@ pub enum Commands { /// Build books with mdbook Book(Arg), + + /// Check one or more usage examples. + /// + /// Usage examples are located in ./examples + UsageExampleCheck(UsageExamplesOpt), + + /// Build one or more usage examples. + /// + /// Usage examples are located in ./examples + #[clap(alias = "./examples")] + UsageExampleBuild(UsageExamplesOpt), +} + +#[derive(Args, Clone, Debug)] +pub struct UsageExamplesOpt { + /// The usage examples to build. All usage examples are selected if this argument is not provided. + /// + /// Example: `rp2040_local_i2c_init,stm32f3_blinky`. + examples: Option<String>, +} + +impl UsageExamplesOpt { + pub fn examples(&self) -> anyhow::Result<Vec<String>> { + let usage_examples: Vec<_> = std::fs::read_dir("./examples")? + .filter_map(Result::ok) + .filter(|p| p.metadata().ok().map(|p| p.is_dir()).unwrap_or(false)) + .filter_map(|p| p.file_name().to_str().map(ToString::to_string)) + .collect(); + + let selected_examples: Option<Vec<String>> = self + .examples + .clone() + .map(|s| s.split(",").map(ToString::to_string).collect()); + + if let Some(selected_examples) = selected_examples { + if let Some(unfound_example) = selected_examples + .iter() + .find(|e| !usage_examples.contains(e)) + { + Err(anyhow::anyhow!( + "Usage example {unfound_example} does not exist" + )) + } else { + Ok(selected_examples) + } + } else { + Ok(usage_examples) + } + } } #[derive(Args, Debug, Clone)] diff --git a/xtask/src/cargo_command.rs b/xtask/src/cargo_command.rs new file mode 100644 index 0000000..1d5f3c5 --- /dev/null +++ b/xtask/src/cargo_command.rs @@ -0,0 +1,750 @@ +use crate::{ExtraArguments, Target}; +use core::fmt; +use std::path::PathBuf; + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BuildMode { + Release, + Debug, +} + +#[derive(Debug)] +pub enum CargoCommand<'a> { + // For future embedded-ci + #[allow(dead_code)] + Run { + cargoarg: &'a Option<&'a str>, + example: &'a str, + target: Option<Target<'a>>, + features: Option<String>, + mode: BuildMode, + dir: Option<PathBuf>, + }, + Qemu { + cargoarg: &'a Option<&'a str>, + example: &'a str, + target: Option<Target<'a>>, + features: Option<String>, + mode: BuildMode, + dir: Option<PathBuf>, + deny_warnings: bool, + }, + ExampleBuild { + cargoarg: &'a Option<&'a str>, + example: &'a str, + target: Option<Target<'a>>, + features: Option<String>, + mode: BuildMode, + dir: Option<PathBuf>, + deny_warnings: bool, + }, + ExampleCheck { + cargoarg: &'a Option<&'a str>, + example: &'a str, + target: Option<Target<'a>>, + features: Option<String>, + mode: BuildMode, + deny_warnings: bool, + }, + Build { + cargoarg: &'a Option<&'a str>, + package: Option<String>, + target: Option<Target<'a>>, + features: Option<String>, + mode: BuildMode, + dir: Option<PathBuf>, + deny_warnings: bool, + }, + Check { + cargoarg: &'a Option<&'a str>, + package: Option<String>, + target: Option<Target<'a>>, + features: Option<String>, + mode: BuildMode, + dir: Option<PathBuf>, + deny_warnings: bool, + }, + Clippy { + cargoarg: &'a Option<&'a str>, + package: Option<String>, + target: Option<Target<'a>>, + features: Option<String>, + deny_warnings: bool, + }, + Format { + cargoarg: &'a Option<&'a str>, + package: Option<String>, + check_only: bool, + }, + Doc { + cargoarg: &'a Option<&'a str>, + features: Option<String>, + arguments: Option<ExtraArguments>, + deny_warnings: bool, + }, + Test { + package: Option<String>, + features: Option<String>, + test: Option<String>, + deny_warnings: bool, + }, + Book { + arguments: Option<ExtraArguments>, + }, + ExampleSize { + cargoarg: &'a Option<&'a str>, + example: &'a str, + target: Option<Target<'a>>, + features: Option<String>, + mode: BuildMode, + arguments: Option<ExtraArguments>, + dir: Option<PathBuf>, + deny_warnings: bool, + }, +} + +impl core::fmt::Display for CargoCommand<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn p(p: &Option<String>) -> String { + if let Some(package) = p { + format!("package {package}") + } else { + format!("default package") + } + } + + fn feat(f: &Option<String>) -> String { + if let Some(features) = f { + format!("\"{features}\"") + } else { + format!("no features") + } + } + + fn carg(f: &&Option<&str>) -> String { + if let Some(cargoarg) = f { + format!("{cargoarg}") + } else { + format!("no cargo args") + } + } + + fn details( + deny_warnings: bool, + target: &Option<Target>, + mode: Option<&BuildMode>, + features: &Option<String>, + cargoarg: &&Option<&str>, + path: Option<&PathBuf>, + ) -> String { + let feat = feat(features); + let carg = carg(cargoarg); + let in_dir = if let Some(path) = path { + let path = path.to_str().unwrap_or("<can't display>"); + format!("in {path}") + } else { + format!("") + }; + + let target = if let Some(target) = target { + format!("{target}") + } else { + format!("<no explicit target>") + }; + + let mode = if let Some(mode) = mode { + format!("{mode}") + } else { + format!("debug") + }; + + let deny_warnings = if deny_warnings { + format!("deny warnings, ") + } else { + format!("") + }; + + if cargoarg.is_some() && path.is_some() { + format!("({deny_warnings}{target}, {mode}, {feat}, {carg}, {in_dir})") + } else if cargoarg.is_some() { + format!("({deny_warnings}{target}, {mode}, {feat}, {carg})") + } else if path.is_some() { + format!("({deny_warnings}{target}, {mode}, {feat}, {in_dir})") + } else { + format!("({deny_warnings}{target}, {mode}, {feat})") + } + } + + match self { + CargoCommand::Run { + cargoarg, + example, + target, + features, + mode, + dir, + } => { + write!( + f, + "Run example {example} {}", + details(false, target, Some(mode), features, cargoarg, dir.as_ref()) + ) + } + CargoCommand::Qemu { + cargoarg, + example, + target, + features, + mode, + dir, + deny_warnings, + } => { + let warns = *deny_warnings; + let details = details(warns, target, Some(mode), features, cargoarg, dir.as_ref()); + write!(f, "Run example {example} in QEMU {details}",) + } + CargoCommand::ExampleBuild { + cargoarg, + example, + target, + features, + mode, + dir, + deny_warnings, + } => { + let warns = *deny_warnings; + let details = details(warns, target, Some(mode), features, cargoarg, dir.as_ref()); + write!(f, "Build example {example} {details}",) + } + CargoCommand::ExampleCheck { + cargoarg, + example, + target, + features, + mode, + deny_warnings, + } => write!( + f, + "Check example {example} {}", + details(*deny_warnings, target, Some(mode), features, cargoarg, None) + ), + CargoCommand::Build { + cargoarg, + package, + target, + features, + mode, + dir, + deny_warnings, + } => { + let package = p(package); + let warns = *deny_warnings; + write!( + f, + "Build {package} {}", + details(warns, target, Some(mode), features, cargoarg, dir.as_ref()) + ) + } + + CargoCommand::Check { + cargoarg, + package, + target, + features, + mode, + dir, + deny_warnings, + } => { + let package = p(package); + let warns = *deny_warnings; + write!( + f, + "Check {package} {}", + details(warns, target, Some(mode), features, cargoarg, dir.as_ref()) + ) + } + CargoCommand::Clippy { + cargoarg, + package, + target, + features, + deny_warnings, + } => { + let details = details(*deny_warnings, target, None, features, cargoarg, None); + let package = p(package); + write!(f, "Clippy {package} {details}") + } + CargoCommand::Format { + cargoarg, + package, + check_only, + } => { + let package = p(package); + let carg = carg(cargoarg); + + let carg = if cargoarg.is_some() { + format!("(cargo args: {carg})") + } else { + format!("") + }; + + if *check_only { + write!(f, "Check format for {package} {carg}") + } else { + write!(f, "Format {package} {carg}") + } + } + CargoCommand::Doc { + cargoarg, + features, + arguments, + deny_warnings, + } => { + let feat = feat(features); + let carg = carg(cargoarg); + let arguments = arguments + .clone() + .map(|a| format!("{a}")) + .unwrap_or_else(|| "no extra arguments".into()); + let deny_warnings = if *deny_warnings { + format!("deny warnings, ") + } else { + format!("") + }; + if cargoarg.is_some() { + write!(f, "Document ({deny_warnings}{feat}, {carg}, {arguments})") + } else { + write!(f, "Document ({deny_warnings}{feat}, {arguments})") + } + } + CargoCommand::Test { + package, + features, + test, + deny_warnings, + } => { + let p = p(package); + let test = test + .clone() + .map(|t| format!("test {t}")) + .unwrap_or("all tests".into()); + let deny_warnings = if *deny_warnings { + format!("deny warnings, ") + } else { + format!("") + }; + let feat = feat(features); + write!(f, "Run {test} in {p} ({deny_warnings}features: {feat})") + } + CargoCommand::Book { arguments: _ } => write!(f, "Build the book"), + CargoCommand::ExampleSize { + cargoarg, + example, + target, + features, + mode, + arguments: _, + dir, + deny_warnings, + } => { + let warns = *deny_warnings; + let details = details(warns, target, Some(mode), features, cargoarg, dir.as_ref()); + write!(f, "Compute size of example {example} {details}") + } + } + } +} + +impl<'a> CargoCommand<'a> { + pub fn as_cmd_string(&self) -> String { + let env = if let Some((key, value)) = self.extra_env() { + format!("{key}=\"{value}\" ") + } else { + format!("") + }; + + let cd = if let Some(Some(chdir)) = self.chdir().map(|p| p.to_str()) { + format!("cd {chdir} && ") + } else { + format!("") + }; + + let executable = self.executable(); + let args = self.args().join(" "); + format!("{env}{cd}{executable} {args}") + } + + fn command(&self) -> &'static str { + match self { + CargoCommand::Run { .. } | CargoCommand::Qemu { .. } => "run", + CargoCommand::ExampleCheck { .. } | CargoCommand::Check { .. } => "check", + CargoCommand::ExampleBuild { .. } | CargoCommand::Build { .. } => "build", + CargoCommand::ExampleSize { .. } => "size", + CargoCommand::Clippy { .. } => "clippy", + CargoCommand::Format { .. } => "fmt", + CargoCommand::Doc { .. } => "doc", + CargoCommand::Book { .. } => "build", + CargoCommand::Test { .. } => "test", + } + } + pub fn executable(&self) -> &'static str { + match self { + CargoCommand::Run { .. } + | CargoCommand::Qemu { .. } + | CargoCommand::ExampleCheck { .. } + | CargoCommand::Check { .. } + | CargoCommand::ExampleBuild { .. } + | CargoCommand::Build { .. } + | CargoCommand::ExampleSize { .. } + | CargoCommand::Clippy { .. } + | CargoCommand::Format { .. } + | CargoCommand::Test { .. } + | CargoCommand::Doc { .. } => "cargo", + CargoCommand::Book { .. } => "mdbook", + } + } + + /// Build args using common arguments for all commands, and the + /// specific information provided + fn build_args<'i, T: Iterator<Item = &'i str>>( + &'i self, + nightly: bool, + cargoarg: &'i Option<&'i str>, + features: &'i Option<String>, + mode: Option<&'i BuildMode>, + extra: T, + ) -> Vec<&str> { + let mut args: Vec<&str> = Vec::new(); + + if nightly { + args.push("+nightly"); + } + + if let Some(cargoarg) = cargoarg.as_deref() { + args.push(cargoarg); + } + + args.push(self.command()); + + if let Some(target) = self.target() { + args.extend_from_slice(&["--target", target.triple()]) + } + + if let Some(features) = features.as_ref() { + args.extend_from_slice(&["--features", features]); + } + + if let Some(mode) = mode.map(|m| m.to_flag()).flatten() { + args.push(mode); + } + + args.extend(extra); + + args + } + + /// Turn the ExtraArguments into an interator that contains the separating dashes + /// and the rest of the arguments. + /// + /// NOTE: you _must_ chain this iterator at the _end_ of the extra arguments. + fn extra_args(args: Option<&ExtraArguments>) -> impl Iterator<Item = &str> { + #[allow(irrefutable_let_patterns)] + let args = if let Some(ExtraArguments::Other(arguments)) = args { + // Extra arguments must be passed after "--" + ["--"] + .into_iter() + .chain(arguments.iter().map(String::as_str)) + .collect() + } else { + vec![] + }; + args.into_iter() + } + + pub fn args(&self) -> Vec<&str> { + fn p(package: &Option<String>) -> impl Iterator<Item = &str> { + if let Some(package) = package { + vec!["--package", &package].into_iter() + } else { + vec![].into_iter() + } + } + + match self { + // For future embedded-ci, for now the same as Qemu + CargoCommand::Run { + cargoarg, + example, + features, + mode, + // dir is exposed through `chdir` + dir: _, + // Target is added by build_args + target: _, + } => self.build_args( + true, + cargoarg, + features, + Some(mode), + ["--example", example].into_iter(), + ), + CargoCommand::Qemu { + cargoarg, + example, + features, + mode, + // dir is exposed through `chdir` + dir: _, + // Target is added by build_args + target: _, + // deny_warnings is exposed through `extra_env` + deny_warnings: _, + } => self.build_args( + true, + cargoarg, + features, + Some(mode), + ["--example", example].into_iter(), + ), + CargoCommand::Build { + cargoarg, + package, + features, + mode, + // Target is added by build_args + target: _, + // Dir is exposed through `chdir` + dir: _, + // deny_warnings is exposed through `extra_env` + deny_warnings: _, + } => self.build_args(true, cargoarg, features, Some(mode), p(package)), + CargoCommand::Check { + cargoarg, + package, + features, + mode, + // Dir is exposed through `chdir` + dir: _, + // Target is added by build_args + target: _, + // deny_warnings is exposed through `extra_env` + deny_warnings: _, + } => self.build_args(true, cargoarg, features, Some(mode), p(package)), + CargoCommand::Clippy { + cargoarg, + package, + features, + // Target is added by build_args + target: _, + deny_warnings, + } => { + let deny_warnings = if *deny_warnings { + vec!["--", "-D", "warnings"] + } else { + vec![] + }; + + let extra = p(package).chain(deny_warnings); + self.build_args(true, cargoarg, features, None, extra) + } + CargoCommand::Doc { + cargoarg, + features, + arguments, + // deny_warnings is exposed through `extra_env` + deny_warnings: _, + } => { + let extra = Self::extra_args(arguments.as_ref()); + self.build_args(true, cargoarg, features, None, extra) + } + CargoCommand::Test { + package, + features, + test, + // deny_warnings is exposed through `extra_env` + deny_warnings: _, + } => { + let extra = if let Some(test) = test { + vec!["--test", test] + } else { + vec![] + }; + let package = p(package); + let extra = extra.into_iter().chain(package); + self.build_args(true, &None, features, None, extra) + } + CargoCommand::Book { arguments } => { + let mut args = vec![]; + + if let Some(ExtraArguments::Other(arguments)) = arguments { + for arg in arguments { + args.extend_from_slice(&[arg.as_str()]); + } + } else { + // If no argument given, run mdbook build + // with default path to book + args.extend_from_slice(&[self.command()]); + args.extend_from_slice(&["book/en"]); + } + args + } + CargoCommand::Format { + cargoarg, + package, + check_only, + } => { + let extra = if *check_only { Some("--check") } else { None }; + let package = p(package); + self.build_args( + true, + cargoarg, + &None, + None, + extra.into_iter().chain(package), + ) + } + CargoCommand::ExampleBuild { + cargoarg, + example, + features, + mode, + // dir is exposed through `chdir` + dir: _, + // Target is added by build_args + target: _, + // deny_warnings is exposed through `extra_env` + deny_warnings: _, + } => self.build_args( + true, + cargoarg, + features, + Some(mode), + ["--example", example].into_iter(), + ), + CargoCommand::ExampleCheck { + cargoarg, + example, + features, + mode, + // Target is added by build_args + target: _, + // deny_warnings is exposed through `extra_env` + deny_warnings: _, + } => self.build_args( + true, + cargoarg, + features, + Some(mode), + ["--example", example].into_iter(), + ), + CargoCommand::ExampleSize { + cargoarg, + example, + features, + mode, + arguments, + // Target is added by build_args + target: _, + // dir is exposed through `chdir` + dir: _, + // deny_warnings is exposed through `extra_env` + deny_warnings: _, + } => { + let extra = ["--example", example] + .into_iter() + .chain(Self::extra_args(arguments.as_ref())); + + self.build_args(true, cargoarg, features, Some(mode), extra) + } + } + } + + /// TODO: integrate this into `args` once `-C` becomes stable. + pub fn chdir(&self) -> Option<&PathBuf> { + match self { + CargoCommand::Qemu { dir, .. } + | CargoCommand::ExampleBuild { dir, .. } + | CargoCommand::ExampleSize { dir, .. } + | CargoCommand::Build { dir, .. } + | CargoCommand::Run { dir, .. } + | CargoCommand::Check { dir, .. } => dir.as_ref(), + _ => None, + } + } + + fn target(&self) -> Option<&Target> { + match self { + CargoCommand::Run { target, .. } + | CargoCommand::Qemu { target, .. } + | CargoCommand::ExampleBuild { target, .. } + | CargoCommand::ExampleCheck { target, .. } + | CargoCommand::Build { target, .. } + | CargoCommand::Check { target, .. } + | CargoCommand::Clippy { target, .. } + | CargoCommand::ExampleSize { target, .. } => target.as_ref(), + _ => None, + } + } + + pub fn extra_env(&self) -> Option<(&str, &str)> { + match self { + // Clippy is a special case: it sets deny warnings + // through an argument to rustc. + CargoCommand::Clippy { .. } => None, + CargoCommand::Doc { .. } => Some(("RUSTDOCFLAGS", "-D warnings")), + + CargoCommand::Qemu { deny_warnings, .. } + | CargoCommand::ExampleBuild { deny_warnings, .. } + | CargoCommand::ExampleSize { deny_warnings, .. } => { + if *deny_warnings { + // NOTE: this also needs the link-arg because .cargo/config.toml + // is ignored if you set the RUSTFLAGS env variable. + Some(("RUSTFLAGS", "-D warnings -C link-arg=-Tlink.x")) + } else { + None + } + } + + CargoCommand::Check { deny_warnings, .. } + | CargoCommand::ExampleCheck { deny_warnings, .. } + | CargoCommand::Build { deny_warnings, .. } + | CargoCommand::Test { deny_warnings, .. } => { + if *deny_warnings { + Some(("RUSTFLAGS", "-D warnings")) + } else { + None + } + } + _ => None, + } + } + + pub fn print_stdout_intermediate(&self) -> bool { + match self { + Self::ExampleSize { .. } => true, + _ => false, + } + } +} + +impl BuildMode { + #[allow(clippy::wrong_self_convention)] + pub fn to_flag(&self) -> Option<&str> { + match self { + BuildMode::Release => Some("--release"), + BuildMode::Debug => None, + } + } +} + +impl fmt::Display for BuildMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cmd = match self { + BuildMode::Release => "release", + BuildMode::Debug => "debug", + }; + + write!(f, "{cmd}") + } +} diff --git a/xtask/src/cargo_commands.rs b/xtask/src/cargo_commands.rs deleted file mode 100644 index 9cbdaef..0000000 --- a/xtask/src/cargo_commands.rs +++ /dev/null @@ -1,345 +0,0 @@ -use crate::{ - argument_parsing::{Backends, BuildOrCheck, ExtraArguments, Globals, PackageOpt, TestMetadata}, - command::{BuildMode, CargoCommand}, - command_parser, RunResult, -}; -use log::error; - -#[cfg(feature = "rayon")] -use rayon::prelude::*; - -use iters::*; - -pub enum FinalRunResult<'c> { - Success(CargoCommand<'c>, RunResult), - Failed(CargoCommand<'c>, RunResult), - CommandError(anyhow::Error), -} - -fn run_and_convert<'a>( - (global, command, overwrite): (&Globals, CargoCommand<'a>, bool), -) -> FinalRunResult<'a> { - // Run the command - let result = command_parser(global, &command, overwrite); - match result { - // If running the command succeeded without looking at any of the results, - // log the data and see if the actual execution was succesfull too. - Ok(result) => { - if result.exit_status.success() { - FinalRunResult::Success(command, result) - } else { - FinalRunResult::Failed(command, result) - } - } - // If it didn't and some IO error occured, just panic - Err(e) => FinalRunResult::CommandError(e), - } -} - -pub trait CoalescingRunner<'c> { - /// Run all the commands in this iterator, and coalesce the results into - /// one error (if any individual commands failed) - fn run_and_coalesce(self) -> Vec<FinalRunResult<'c>>; -} - -#[cfg(not(feature = "rayon"))] -mod iters { - use super::*; - - pub fn examples_iter(examples: &[String]) -> impl Iterator<Item = &String> { - examples.into_iter() - } - - impl<'g, 'c, I> CoalescingRunner<'c> for I - where - I: Iterator<Item = (&'g Globals, CargoCommand<'c>, bool)>, - { - fn run_and_coalesce(self) -> Vec<FinalRunResult<'c>> { - self.map(run_and_convert).collect() - } - } -} - -#[cfg(feature = "rayon")] -mod iters { - use super::*; - - pub fn examples_iter(examples: &[String]) -> impl ParallelIterator<Item = &String> { - examples.into_par_iter() - } - - impl<'g, 'c, I> CoalescingRunner<'c> for I - where - I: ParallelIterator<Item = (&'g Globals, CargoCommand<'c>, bool)>, - { - fn run_and_coalesce(self) -> Vec<FinalRunResult<'c>> { - self.map(run_and_convert).collect() - } - } -} - -/// Cargo command to either build or check -pub fn cargo<'c>( - globals: &Globals, - operation: BuildOrCheck, - cargoarg: &'c Option<&'c str>, - package: &'c PackageOpt, - backend: Backends, -) -> Vec<FinalRunResult<'c>> { - let runner = package - .packages() - .flat_map(|package| { - let target = backend.to_target(); - let features = package.features(target, backend, globals.partial); - - #[cfg(feature = "rayon")] - { - features.into_par_iter().map(move |f| (package, target, f)) - } - - #[cfg(not(feature = "rayon"))] - { - features.into_iter().map(move |f| (package, target, f)) - } - }) - .map(move |(package, target, features)| { - let command = match operation { - BuildOrCheck::Check => CargoCommand::Check { - cargoarg, - package: Some(package), - target, - features, - mode: BuildMode::Release, - }, - BuildOrCheck::Build => CargoCommand::Build { - cargoarg, - package: Some(package), - target, - features, - mode: BuildMode::Release, - }, - }; - - (globals, command, false) - }); - - runner.run_and_coalesce() -} - -/// Cargo command to either build or check all examples -/// -/// The examples are in rtic/examples -pub fn cargo_example<'c>( - globals: &Globals, - operation: BuildOrCheck, - cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], -) -> Vec<FinalRunResult<'c>> { - let runner = examples_iter(examples).map(|example| { - let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); - - let command = match operation { - BuildOrCheck::Check => CargoCommand::ExampleCheck { - cargoarg, - example, - target: backend.to_target(), - features, - mode: BuildMode::Release, - }, - BuildOrCheck::Build => CargoCommand::ExampleBuild { - cargoarg, - example, - target: backend.to_target(), - features, - mode: BuildMode::Release, - }, - }; - (globals, command, false) - }); - runner.run_and_coalesce() -} - -/// Run cargo clippy on selected package -pub fn cargo_clippy<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - package: &'c PackageOpt, - backend: Backends, -) -> Vec<FinalRunResult<'c>> { - let runner = package - .packages() - .flat_map(|package| { - let target = backend.to_target(); - let features = package.features(target, backend, globals.partial); - - #[cfg(feature = "rayon")] - { - features.into_par_iter().map(move |f| (package, target, f)) - } - - #[cfg(not(feature = "rayon"))] - { - features.into_iter().map(move |f| (package, target, f)) - } - }) - .map(move |(package, target, features)| { - ( - globals, - CargoCommand::Clippy { - cargoarg, - package: Some(package), - target, - features, - }, - false, - ) - }); - - runner.run_and_coalesce() -} - -/// Run cargo fmt on selected package -pub fn cargo_format<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - package: &'c PackageOpt, - check_only: bool, -) -> Vec<FinalRunResult<'c>> { - let runner = package.packages().map(|p| { - ( - globals, - CargoCommand::Format { - cargoarg, - package: Some(p), - check_only, - }, - false, - ) - }); - runner.run_and_coalesce() -} - -/// Run cargo doc -pub fn cargo_doc<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - backend: Backends, - arguments: &'c Option<ExtraArguments>, -) -> Vec<FinalRunResult<'c>> { - let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); - - let command = CargoCommand::Doc { - cargoarg, - features, - arguments: arguments.clone(), - }; - - vec![run_and_convert((globals, command, false))] -} - -/// Run cargo test on the selected package or all packages -/// -/// If no package is specified, loop through all packages -pub fn cargo_test<'c>( - globals: &Globals, - package: &'c PackageOpt, - backend: Backends, -) -> Vec<FinalRunResult<'c>> { - package - .packages() - .map(|p| (globals, TestMetadata::match_package(p, backend), false)) - .run_and_coalesce() -} - -/// Use mdbook to build the book -pub fn cargo_book<'c>( - globals: &Globals, - arguments: &'c Option<ExtraArguments>, -) -> Vec<FinalRunResult<'c>> { - vec![run_and_convert(( - globals, - CargoCommand::Book { - arguments: arguments.clone(), - }, - false, - ))] -} - -/// Run examples -/// -/// Supports updating the expected output via the overwrite argument -pub fn run_test<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], - overwrite: bool, -) -> Vec<FinalRunResult<'c>> { - let target = backend.to_target(); - let features = Some(target.and_features(backend.to_rtic_feature())); - - examples_iter(examples) - .map(|example| { - let cmd = CargoCommand::ExampleBuild { - cargoarg: &Some("--quiet"), - example, - target, - features: features.clone(), - mode: BuildMode::Release, - }; - - if let Err(err) = command_parser(globals, &cmd, false) { - error!("{err}"); - } - - let cmd = CargoCommand::Qemu { - cargoarg, - example, - target, - features: features.clone(), - mode: BuildMode::Release, - }; - - (globals, cmd, overwrite) - }) - .run_and_coalesce() -} - -/// Check the binary sizes of examples -pub fn build_and_check_size<'c>( - globals: &Globals, - cargoarg: &'c Option<&'c str>, - backend: Backends, - examples: &'c [String], - arguments: &'c Option<ExtraArguments>, -) -> Vec<FinalRunResult<'c>> { - let target = backend.to_target(); - let features = Some(target.and_features(backend.to_rtic_feature())); - - let runner = examples_iter(examples).map(|example| { - // Make sure the requested example(s) are built - let cmd = CargoCommand::ExampleBuild { - cargoarg: &Some("--quiet"), - example, - target, - features: features.clone(), - mode: BuildMode::Release, - }; - if let Err(err) = command_parser(globals, &cmd, false) { - error!("{err}"); - } - - let cmd = CargoCommand::ExampleSize { - cargoarg, - example, - target: backend.to_target(), - features: features.clone(), - mode: BuildMode::Release, - arguments: arguments.clone(), - }; - (globals, cmd, false) - }); - - runner.run_and_coalesce() -} diff --git a/xtask/src/command.rs b/xtask/src/command.rs deleted file mode 100644 index b62724a..0000000 --- a/xtask/src/command.rs +++ /dev/null @@ -1,779 +0,0 @@ -use log::{error, info, Level}; - -use crate::{ - argument_parsing::Globals, cargo_commands::FinalRunResult, ExtraArguments, Package, RunResult, - Target, TestRunError, -}; -use core::fmt; -use std::{ - fs::File, - io::Read, - process::{Command, Stdio}, -}; - -#[allow(dead_code)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum BuildMode { - Release, - Debug, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum OutputMode { - PipedAndCollected, - Inherited, -} - -impl From<OutputMode> for Stdio { - fn from(value: OutputMode) -> Self { - match value { - OutputMode::PipedAndCollected => Stdio::piped(), - OutputMode::Inherited => Stdio::inherit(), - } - } -} - -#[derive(Debug)] -pub enum CargoCommand<'a> { - // For future embedded-ci - #[allow(dead_code)] - Run { - cargoarg: &'a Option<&'a str>, - example: &'a str, - target: Target<'a>, - features: Option<String>, - mode: BuildMode, - }, - Qemu { - cargoarg: &'a Option<&'a str>, - example: &'a str, - target: Target<'a>, - features: Option<String>, - mode: BuildMode, - }, - ExampleBuild { - cargoarg: &'a Option<&'a str>, - example: &'a str, - target: Target<'a>, - features: Option<String>, - mode: BuildMode, - }, - ExampleCheck { - cargoarg: &'a Option<&'a str>, - example: &'a str, - target: Target<'a>, - features: Option<String>, - mode: BuildMode, - }, - Build { - cargoarg: &'a Option<&'a str>, - package: Option<Package>, - target: Target<'a>, - features: Option<String>, - mode: BuildMode, - }, - Check { - cargoarg: &'a Option<&'a str>, - package: Option<Package>, - target: Target<'a>, - features: Option<String>, - mode: BuildMode, - }, - Clippy { - cargoarg: &'a Option<&'a str>, - package: Option<Package>, - target: Target<'a>, - features: Option<String>, - }, - Format { - cargoarg: &'a Option<&'a str>, - package: Option<Package>, - check_only: bool, - }, - Doc { - cargoarg: &'a Option<&'a str>, - features: Option<String>, - arguments: Option<ExtraArguments>, - }, - Test { - package: Option<Package>, - features: Option<String>, - test: Option<String>, - }, - Book { - arguments: Option<ExtraArguments>, - }, - ExampleSize { - cargoarg: &'a Option<&'a str>, - example: &'a str, - target: Target<'a>, - features: Option<String>, - mode: BuildMode, - arguments: Option<ExtraArguments>, - }, -} - -impl core::fmt::Display for CargoCommand<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let p = |p: &Option<Package>| { - if let Some(package) = p { - format!("package {package}") - } else { - format!("default package") - } - }; - - let feat = |f: &Option<String>| { - if let Some(features) = f { - format!("\"{features}\"") - } else { - format!("no features") - } - }; - - let carg = |f: &&Option<&str>| { - if let Some(cargoarg) = f { - format!("{cargoarg}") - } else { - format!("no cargo args") - } - }; - - let details = |target: &Target, - mode: &BuildMode, - features: &Option<String>, - cargoarg: &&Option<&str>| { - let feat = feat(features); - let carg = carg(cargoarg); - if cargoarg.is_some() { - format!("({target}, {mode}, {feat}, {carg})") - } else { - format!("({target}, {mode}, {feat})") - } - }; - - match self { - CargoCommand::Run { - cargoarg, - example, - target, - features, - mode, - } => write!( - f, - "Run example {example} {}", - details(target, mode, features, cargoarg) - ), - CargoCommand::Qemu { - cargoarg, - example, - target, - features, - mode, - } => write!( - f, - "Run example {example} in QEMU {}", - details(target, mode, features, cargoarg) - ), - CargoCommand::ExampleBuild { - cargoarg, - example, - target, - features, - mode, - } => write!( - f, - "Build example {example} {}", - details(target, mode, features, cargoarg) - ), - CargoCommand::ExampleCheck { - cargoarg, - example, - target, - features, - mode, - } => write!( - f, - "Check example {example} {}", - details(target, mode, features, cargoarg) - ), - CargoCommand::Build { - cargoarg, - package, - target, - features, - mode, - } => { - let package = p(package); - write!( - f, - "Build {package} {}", - details(target, mode, features, cargoarg) - ) - } - CargoCommand::Check { - cargoarg, - package, - target, - features, - mode, - } => { - let package = p(package); - write!( - f, - "Check {package} {}", - details(target, mode, features, cargoarg) - ) - } - CargoCommand::Clippy { - cargoarg, - package, - target, - features, - } => { - let package = p(package); - let features = feat(features); - let carg = carg(cargoarg); - if cargoarg.is_some() { - write!(f, "Clippy {package} ({target}, {features}, {carg})") - } else { - write!(f, "Clippy {package} ({target}, {features})") - } - } - CargoCommand::Format { - cargoarg, - package, - check_only, - } => { - let package = p(package); - let carg = carg(cargoarg); - - let carg = if cargoarg.is_some() { - format!("(cargo args: {carg})") - } else { - format!("") - }; - - if *check_only { - write!(f, "Check format for {package} {carg}") - } else { - write!(f, "Format {package} {carg}") - } - } - CargoCommand::Doc { - cargoarg, - features, - arguments, - } => { - let feat = feat(features); - let carg = carg(cargoarg); - let arguments = arguments - .clone() - .map(|a| format!("{a}")) - .unwrap_or_else(|| "no extra arguments".into()); - if cargoarg.is_some() { - write!(f, "Document ({feat}, {carg}, {arguments})") - } else { - write!(f, "Document ({feat}, {arguments})") - } - } - CargoCommand::Test { - package, - features, - test, - } => { - let p = p(package); - let test = test - .clone() - .map(|t| format!("test {t}")) - .unwrap_or("all tests".into()); - let feat = feat(features); - write!(f, "Run {test} in {p} (features: {feat})") - } - CargoCommand::Book { arguments: _ } => write!(f, "Build the book"), - CargoCommand::ExampleSize { - cargoarg, - example, - target, - features, - mode, - arguments: _, - } => { - write!( - f, - "Compute size of example {example} {}", - details(target, mode, features, cargoarg) - ) - } - } - } -} - -impl<'a> CargoCommand<'a> { - pub fn as_cmd_string(&self) -> String { - let executable = self.executable(); - let args = self.args().join(" "); - format!("{executable} {args}") - } - - fn command(&self) -> &str { - match self { - CargoCommand::Run { .. } | CargoCommand::Qemu { .. } => "run", - CargoCommand::ExampleCheck { .. } | CargoCommand::Check { .. } => "check", - CargoCommand::ExampleBuild { .. } | CargoCommand::Build { .. } => "build", - CargoCommand::ExampleSize { .. } => "size", - CargoCommand::Clippy { .. } => "clippy", - CargoCommand::Format { .. } => "fmt", - CargoCommand::Doc { .. } => "doc", - CargoCommand::Book { .. } => "build", - CargoCommand::Test { .. } => "test", - } - } - pub fn executable(&self) -> &str { - match self { - CargoCommand::Run { .. } - | CargoCommand::Qemu { .. } - | CargoCommand::ExampleCheck { .. } - | CargoCommand::Check { .. } - | CargoCommand::ExampleBuild { .. } - | CargoCommand::Build { .. } - | CargoCommand::ExampleSize { .. } - | CargoCommand::Clippy { .. } - | CargoCommand::Format { .. } - | CargoCommand::Test { .. } - | CargoCommand::Doc { .. } => "cargo", - CargoCommand::Book { .. } => "mdbook", - } - } - - pub fn args(&self) -> Vec<&str> { - match self { - // For future embedded-ci, for now the same as Qemu - CargoCommand::Run { - cargoarg, - example, - target, - features, - mode, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - args.extend_from_slice(&[ - self.command(), - "--example", - example, - "--target", - target.triple(), - ]); - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - if let Some(flag) = mode.to_flag() { - args.push(flag); - } - args - } - CargoCommand::Qemu { - cargoarg, - example, - target, - features, - mode, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - args.extend_from_slice(&[ - self.command(), - "--example", - example, - "--target", - target.triple(), - ]); - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - if let Some(flag) = mode.to_flag() { - args.push(flag); - } - args - } - CargoCommand::Build { - cargoarg, - package, - target, - features, - mode, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - - args.extend_from_slice(&[self.command(), "--target", target.triple()]); - - if let Some(package) = package { - args.extend_from_slice(&["--package", package.name()]); - } - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - if let Some(flag) = mode.to_flag() { - args.push(flag); - } - args - } - CargoCommand::Check { - cargoarg, - package, - target: _, - features, - mode, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - args.extend_from_slice(&[self.command()]); - - if let Some(package) = package { - args.extend_from_slice(&["--package", package.name()]); - } - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - if let Some(flag) = mode.to_flag() { - args.push(flag); - } - args - } - CargoCommand::Clippy { - cargoarg, - package, - target: _, - features, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - - args.extend_from_slice(&[self.command()]); - - if let Some(package) = package { - args.extend_from_slice(&["--package", package.name()]); - } - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - args - } - CargoCommand::Doc { - cargoarg, - features, - arguments, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - - args.extend_from_slice(&[self.command()]); - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - if let Some(ExtraArguments::Other(arguments)) = arguments { - for arg in arguments { - args.extend_from_slice(&[arg.as_str()]); - } - } - args - } - CargoCommand::Test { - package, - features, - test, - } => { - let mut args = vec!["+nightly"]; - args.extend_from_slice(&[self.command()]); - - if let Some(package) = package { - args.extend_from_slice(&["--package", package.name()]); - } - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - if let Some(test) = test { - args.extend_from_slice(&["--test", test]); - } - args - } - CargoCommand::Book { arguments } => { - let mut args = vec![]; - - if let Some(ExtraArguments::Other(arguments)) = arguments { - for arg in arguments { - args.extend_from_slice(&[arg.as_str()]); - } - } else { - // If no argument given, run mdbook build - // with default path to book - args.extend_from_slice(&[self.command()]); - args.extend_from_slice(&["book/en"]); - } - args - } - CargoCommand::Format { - cargoarg, - package, - check_only, - } => { - let mut args = vec!["+nightly", self.command()]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - - if let Some(package) = package { - args.extend_from_slice(&["--package", package.name()]); - } - if *check_only { - args.extend_from_slice(&["--check"]); - } - - args - } - CargoCommand::ExampleBuild { - cargoarg, - example, - target, - features, - mode, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - args.extend_from_slice(&[ - self.command(), - "--example", - example, - "--target", - target.triple(), - ]); - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - if let Some(flag) = mode.to_flag() { - args.push(flag); - } - args - } - CargoCommand::ExampleCheck { - cargoarg, - example, - target, - features, - mode, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - args.extend_from_slice(&[ - self.command(), - "--example", - example, - "--target", - target.triple(), - ]); - - if let Some(feature) = features { - args.extend_from_slice(&["--features", feature]); - } - if let Some(flag) = mode.to_flag() { - args.push(flag); - } - args - } - CargoCommand::ExampleSize { - cargoarg, - example, - target, - features, - mode, - arguments, - } => { - let mut args = vec!["+nightly"]; - if let Some(cargoarg) = cargoarg { - args.extend_from_slice(&[cargoarg]); - } - args.extend_from_slice(&[ - self.command(), - "--example", - example, - "--target", - target.triple(), - ]); - - if let Some(feature_name) = features { - args.extend_from_slice(&["--features", feature_name]); - } - if let Some(flag) = mode.to_flag() { - args.push(flag); - } - if let Some(ExtraArguments::Other(arguments)) = arguments { - // Arguments to cargo size must be passed after "--" - args.extend_from_slice(&["--"]); - for arg in arguments { - args.extend_from_slice(&[arg.as_str()]); - } - } - args - } - } - } -} - -impl BuildMode { - #[allow(clippy::wrong_self_convention)] - pub fn to_flag(&self) -> Option<&str> { - match self { - BuildMode::Release => Some("--release"), - BuildMode::Debug => None, - } - } -} - -impl fmt::Display for BuildMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let cmd = match self { - BuildMode::Release => "release", - BuildMode::Debug => "debug", - }; - - write!(f, "{cmd}") - } -} - -pub fn run_command(command: &CargoCommand, stderr_mode: OutputMode) -> anyhow::Result<RunResult> { - log::info!("👟 {command}"); - - let result = Command::new(command.executable()) - .args(command.args()) - .stdout(Stdio::piped()) - .stderr(stderr_mode) - .output()?; - - let exit_status = result.status; - let stderr = String::from_utf8(result.stderr).unwrap_or("Not displayable".into()); - let stdout = String::from_utf8(result.stdout).unwrap_or("Not displayable".into()); - - Ok(RunResult { - exit_status, - stdout, - stderr, - }) -} - -/// Check if `run` was successful. -/// returns Ok in case the run went as expected, -/// Err otherwise -pub fn run_successful(run: &RunResult, expected_output_file: &str) -> Result<(), TestRunError> { - let mut file_handle = - File::open(expected_output_file).map_err(|_| TestRunError::FileError { - file: expected_output_file.to_owned(), - })?; - let mut expected_output = String::new(); - file_handle - .read_to_string(&mut expected_output) - .map_err(|_| TestRunError::FileError { - file: expected_output_file.to_owned(), - })?; - - if expected_output != run.stdout { - Err(TestRunError::FileCmpError { - expected: expected_output.clone(), - got: run.stdout.clone(), - }) - } else if !run.exit_status.success() { - Err(TestRunError::CommandError(run.clone())) - } else { - Ok(()) - } -} - -pub fn handle_results(globals: &Globals, results: Vec<FinalRunResult>) -> anyhow::Result<()> { - let errors = results.iter().filter_map(|r| { - if let FinalRunResult::Failed(c, r) = r { - Some((c, r)) - } else { - None - } - }); - - let successes = results.iter().filter_map(|r| { - if let FinalRunResult::Success(c, r) = r { - Some((c, r)) - } else { - None - } - }); - - let log_stdout_stderr = |level: Level| { - move |(command, result): (&CargoCommand, &RunResult)| { - let stdout = &result.stdout; - let stderr = &result.stderr; - if !stdout.is_empty() && !stderr.is_empty() { - log::log!( - level, - "Output for \"{command}\"\nStdout:\n{stdout}\nStderr:\n{stderr}" - ); - } else if !stdout.is_empty() { - log::log!( - level, - "Output for \"{command}\":\nStdout:\n{}", - stdout.trim_end() - ); - } else if !stderr.is_empty() { - log::log!( - level, - "Output for \"{command}\"\nStderr:\n{}", - stderr.trim_end() - ); - } - } - }; - - successes.clone().for_each(log_stdout_stderr(Level::Debug)); - errors.clone().for_each(log_stdout_stderr(Level::Error)); - - successes.for_each(|(cmd, _)| { - if globals.verbose > 0 { - info!("✅ Success: {cmd}\n {}", cmd.as_cmd_string()); - } else { - info!("✅ Success: {cmd}"); - } - }); - - errors.clone().for_each(|(cmd, _)| { - error!("❌ Failed: {cmd}\n {}", cmd.as_cmd_string()); - }); - - let ecount = errors.count(); - if ecount != 0 { - Err(anyhow::anyhow!("{ecount} commands failed.")) - } else { - Ok(()) - } -} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 2bfe851..30c3da0 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,33 +1,19 @@ mod argument_parsing; mod build; -mod cargo_commands; -mod command; +mod cargo_command; +mod run; -use argument_parsing::{ExtraArguments, Globals, Package}; +use argument_parsing::ExtraArguments; use clap::Parser; -use command::OutputMode; use core::fmt; -use diffy::{create_patch, PatchFormatter}; -use std::{ - error::Error, - ffi::OsString, - fs::File, - io::prelude::*, - path::{Path, PathBuf}, - process::ExitStatus, - str, -}; +use std::{path::Path, str}; use log::{error, info, log_enabled, trace, Level}; use crate::{ argument_parsing::{Backends, BuildOrCheck, Cli, Commands}, build::init_build_dir, - cargo_commands::{ - build_and_check_size, cargo, cargo_book, cargo_clippy, cargo_doc, cargo_example, - cargo_format, cargo_test, run_test, - }, - command::{handle_results, run_command, run_successful, CargoCommand}, + run::*, }; #[derive(Debug, Clone, Copy)] @@ -69,56 +55,6 @@ const ARMV7M: Target = Target::new("thumbv7m-none-eabi", false); const ARMV8MBASE: Target = Target::new("thumbv8m.base-none-eabi", false); const ARMV8MMAIN: Target = Target::new("thumbv8m.main-none-eabi", false); -#[derive(Debug, Clone)] -pub struct RunResult { - exit_status: ExitStatus, - stdout: String, - stderr: String, -} - -#[derive(Debug)] -pub enum TestRunError { - FileCmpError { expected: String, got: String }, - FileError { file: String }, - PathConversionError(OsString), - CommandError(RunResult), - IncompatibleCommand, -} -impl fmt::Display for TestRunError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TestRunError::FileCmpError { expected, got } => { - let patch = create_patch(expected, got); - writeln!(f, "Differing output in files.\n")?; - let pf = PatchFormatter::new().with_color(); - writeln!(f, "{}", pf.fmt_patch(&patch))?; - write!( - f, - "See flag --overwrite-expected to create/update expected output." - ) - } - TestRunError::FileError { file } => { - write!(f, "File error on: {file}\nSee flag --overwrite-expected to create/update expected output.") - } - TestRunError::CommandError(e) => { - write!( - f, - "Command failed with exit status {}: {}", - e.exit_status, e.stdout - ) - } - TestRunError::PathConversionError(p) => { - write!(f, "Can't convert path from `OsString` to `String`: {p:?}") - } - TestRunError::IncompatibleCommand => { - write!(f, "Can't run that command in this context") - } - } - } -} - -impl Error for TestRunError {} - fn main() -> anyhow::Result<()> { // if there's an `xtask` folder, we're *probably* at the root of this repo (we can't just // check the name of `env::current_dir()` because people might clone it into a different name) @@ -152,6 +88,12 @@ fn main() -> anyhow::Result<()> { trace!("default logging level: {0}", globals.verbose); + log::debug!( + "Stderr of child processes is inherited: {}", + globals.stderr_inherited + ); + log::debug!("Partial features: {}", globals.partial); + let backend = if let Some(backend) = globals.backend { backend } else { @@ -265,7 +207,7 @@ fn main() -> anyhow::Result<()> { Commands::Qemu(args) | Commands::Run(args) => { // x86_64 target not valid info!("Testing for backend: {backend:?}"); - run_test( + qemu_run_examples( globals, &cargologlevel, backend, @@ -285,71 +227,15 @@ fn main() -> anyhow::Result<()> { info!("Running mdbook"); cargo_book(globals, &args.arguments) } - }; - - handle_results(globals, final_run_results) -} - -// run example binary `example` -fn command_parser( - glob: &Globals, - command: &CargoCommand, - overwrite: bool, -) -> anyhow::Result<RunResult> { - let output_mode = if glob.stderr_inherited { - OutputMode::Inherited - } else { - OutputMode::PipedAndCollected - }; - - match *command { - CargoCommand::Qemu { example, .. } | CargoCommand::Run { example, .. } => { - let run_file = format!("{example}.run"); - let expected_output_file = ["rtic", "ci", "expected", &run_file] - .iter() - .collect::<PathBuf>() - .into_os_string() - .into_string() - .map_err(TestRunError::PathConversionError)?; - - // cargo run <..> - info!("Running example: {example}"); - let cargo_run_result = run_command(command, output_mode)?; - info!("{}", cargo_run_result.stdout); - - // Create a file for the expected output if it does not exist or mismatches - if overwrite { - let result = run_successful(&cargo_run_result, &expected_output_file); - if let Err(e) = result { - // FileError means the file did not exist or was unreadable - error!("Error: {e}"); - let mut file_handle = File::create(&expected_output_file).map_err(|_| { - TestRunError::FileError { - file: expected_output_file.clone(), - } - })?; - info!("Flag --overwrite-expected enabled"); - info!("Creating/updating file: {expected_output_file}"); - file_handle.write_all(cargo_run_result.stdout.as_bytes())?; - }; - } else { - run_successful(&cargo_run_result, &expected_output_file)?; - }; - - Ok(cargo_run_result) + Commands::UsageExampleCheck(examples) => { + info!("Checking usage examples"); + cargo_usage_example(globals, BuildOrCheck::Check, examples.examples()?) } - CargoCommand::Format { .. } - | CargoCommand::ExampleCheck { .. } - | CargoCommand::ExampleBuild { .. } - | CargoCommand::Check { .. } - | CargoCommand::Build { .. } - | CargoCommand::Clippy { .. } - | CargoCommand::Doc { .. } - | CargoCommand::Test { .. } - | CargoCommand::Book { .. } - | CargoCommand::ExampleSize { .. } => { - let cargo_result = run_command(command, output_mode)?; - Ok(cargo_result) + Commands::UsageExampleBuild(examples) => { + info!("Building usage examples"); + cargo_usage_example(globals, BuildOrCheck::Build, examples.examples()?) } - } + }; + + handle_results(globals, final_run_results).map_err(|_| anyhow::anyhow!("Commands failed")) } diff --git a/xtask/src/run.rs b/xtask/src/run.rs new file mode 100644 index 0000000..6057551 --- /dev/null +++ b/xtask/src/run.rs @@ -0,0 +1,501 @@ +use std::{ + fs::File, + io::Write, + path::PathBuf, + process::{Command, Stdio}, +}; + +mod results; +pub use results::handle_results; + +mod data; +use data::*; + +mod iter; +use iter::{into_iter, CoalescingRunner}; + +use crate::{ + argument_parsing::{Backends, BuildOrCheck, ExtraArguments, Globals, PackageOpt, TestMetadata}, + cargo_command::{BuildMode, CargoCommand}, +}; + +use log::{error, info}; + +#[cfg(feature = "rayon")] +use rayon::prelude::*; + +fn run_and_convert<'a>( + (global, command, overwrite): (&Globals, CargoCommand<'a>, bool), +) -> FinalRunResult<'a> { + // Run the command + let result = command_parser(global, &command, overwrite); + + let output = match result { + // If running the command succeeded without looking at any of the results, + // log the data and see if the actual execution was succesfull too. + Ok(result) => { + if result.exit_status.success() { + FinalRunResult::Success(command, result) + } else { + FinalRunResult::Failed(command, result) + } + } + // If it didn't and some IO error occured, just panic + Err(e) => FinalRunResult::CommandError(command, e), + }; + + log::trace!("Final result: {output:?}"); + + output +} + +// run example binary `example` +fn command_parser( + glob: &Globals, + command: &CargoCommand, + overwrite: bool, +) -> anyhow::Result<RunResult> { + let output_mode = if glob.stderr_inherited { + OutputMode::Inherited + } else { + OutputMode::PipedAndCollected + }; + + match *command { + CargoCommand::Qemu { example, .. } | CargoCommand::Run { example, .. } => { + /// Check if `run` was successful. + /// returns Ok in case the run went as expected, + /// Err otherwise + pub fn run_successful( + run: &RunResult, + expected_output_file: &str, + ) -> Result<(), TestRunError> { + let file = expected_output_file.to_string(); + + let expected_output = std::fs::read(expected_output_file) + .map(|d| { + String::from_utf8(d) + .map_err(|_| TestRunError::FileError { file: file.clone() }) + }) + .map_err(|_| TestRunError::FileError { file })??; + + let res = if expected_output != run.stdout { + Err(TestRunError::FileCmpError { + expected: expected_output.clone(), + got: run.stdout.clone(), + }) + } else if !run.exit_status.success() { + Err(TestRunError::CommandError(run.clone())) + } else { + Ok(()) + }; + + if res.is_ok() { + log::info!("✅ Success."); + } else { + log::error!("❌ Command failed. Run to completion for the summary."); + } + + res + } + + let run_file = format!("{example}.run"); + let expected_output_file = ["rtic", "ci", "expected", &run_file] + .iter() + .collect::<PathBuf>() + .into_os_string() + .into_string() + .map_err(TestRunError::PathConversionError)?; + + // cargo run <..> + let cargo_run_result = run_command(command, output_mode, false)?; + + // Create a file for the expected output if it does not exist or mismatches + if overwrite { + let result = run_successful(&cargo_run_result, &expected_output_file); + if let Err(e) = result { + // FileError means the file did not exist or was unreadable + error!("Error: {e}"); + let mut file_handle = File::create(&expected_output_file).map_err(|_| { + TestRunError::FileError { + file: expected_output_file.clone(), + } + })?; + info!("Flag --overwrite-expected enabled"); + info!("Creating/updating file: {expected_output_file}"); + file_handle.write_all(cargo_run_result.stdout.as_bytes())?; + }; + } else { + run_successful(&cargo_run_result, &expected_output_file)?; + }; + + Ok(cargo_run_result) + } + CargoCommand::Format { .. } + | CargoCommand::ExampleCheck { .. } + | CargoCommand::ExampleBuild { .. } + | CargoCommand::Check { .. } + | CargoCommand::Build { .. } + | CargoCommand::Clippy { .. } + | CargoCommand::Doc { .. } + | CargoCommand::Test { .. } + | CargoCommand::Book { .. } + | CargoCommand::ExampleSize { .. } => { + let cargo_result = run_command(command, output_mode, true)?; + Ok(cargo_result) + } + } +} + +/// Cargo command to either build or check +pub fn cargo<'c>( + globals: &Globals, + operation: BuildOrCheck, + cargoarg: &'c Option<&'c str>, + package: &'c PackageOpt, + backend: Backends, +) -> Vec<FinalRunResult<'c>> { + let runner = package + .packages() + .flat_map(|package| { + let target = backend.to_target(); + let features = package.features(target, backend, globals.partial); + into_iter(features).map(move |f| (package, target, f)) + }) + .map(move |(package, target, features)| { + let target = target.into(); + let command = match operation { + BuildOrCheck::Check => CargoCommand::Check { + cargoarg, + package: Some(package.name()), + target, + features, + mode: BuildMode::Release, + dir: None, + deny_warnings: globals.deny_warnings, + }, + BuildOrCheck::Build => CargoCommand::Build { + cargoarg, + package: Some(package.name()), + target, + features, + mode: BuildMode::Release, + dir: None, + deny_warnings: globals.deny_warnings, + }, + }; + + (globals, command, false) + }); + + runner.run_and_coalesce() +} + +/// Cargo command to build a usage example. +/// +/// The usage examples are in examples/ +pub fn cargo_usage_example( + globals: &Globals, + operation: BuildOrCheck, + usage_examples: Vec<String>, +) -> Vec<FinalRunResult<'_>> { + into_iter(&usage_examples) + .map(|example| { + let path = format!("examples/{example}"); + + let command = match operation { + BuildOrCheck::Check => CargoCommand::Check { + cargoarg: &None, + mode: BuildMode::Release, + dir: Some(path.into()), + package: None, + target: None, + features: None, + deny_warnings: globals.deny_warnings, + }, + BuildOrCheck::Build => CargoCommand::Build { + cargoarg: &None, + package: None, + target: None, + features: None, + mode: BuildMode::Release, + dir: Some(path.into()), + deny_warnings: globals.deny_warnings, + }, + }; + (globals, command, false) + }) + .run_and_coalesce() +} + +/// Cargo command to either build or check all examples +/// +/// The examples are in rtic/examples +pub fn cargo_example<'c>( + globals: &Globals, + operation: BuildOrCheck, + cargoarg: &'c Option<&'c str>, + backend: Backends, + examples: &'c [String], +) -> Vec<FinalRunResult<'c>> { + let runner = into_iter(examples).map(|example| { + let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); + + let command = match operation { + BuildOrCheck::Check => CargoCommand::ExampleCheck { + cargoarg, + example, + target: Some(backend.to_target()), + features, + mode: BuildMode::Release, + deny_warnings: globals.deny_warnings, + }, + BuildOrCheck::Build => CargoCommand::ExampleBuild { + cargoarg, + example, + target: Some(backend.to_target()), + features, + mode: BuildMode::Release, + dir: Some(PathBuf::from("./rtic")), + deny_warnings: globals.deny_warnings, + }, + }; + (globals, command, false) + }); + runner.run_and_coalesce() +} + +/// Run cargo clippy on selected package +pub fn cargo_clippy<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + package: &'c PackageOpt, + backend: Backends, +) -> Vec<FinalRunResult<'c>> { + let runner = package + .packages() + .flat_map(|package| { + let target = backend.to_target(); + let features = package.features(target, backend, globals.partial); + into_iter(features).map(move |f| (package, target, f)) + }) + .map(move |(package, target, features)| { + let command = CargoCommand::Clippy { + cargoarg, + package: Some(package.name()), + target: target.into(), + features, + deny_warnings: true, + }; + + (globals, command, false) + }); + + runner.run_and_coalesce() +} + +/// Run cargo fmt on selected package +pub fn cargo_format<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + package: &'c PackageOpt, + check_only: bool, +) -> Vec<FinalRunResult<'c>> { + let runner = package.packages().map(|p| { + ( + globals, + CargoCommand::Format { + cargoarg, + package: Some(p.name()), + check_only, + }, + false, + ) + }); + runner.run_and_coalesce() +} + +/// Run cargo doc +pub fn cargo_doc<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + backend: Backends, + arguments: &'c Option<ExtraArguments>, +) -> Vec<FinalRunResult<'c>> { + let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); + + let command = CargoCommand::Doc { + cargoarg, + features, + arguments: arguments.clone(), + deny_warnings: true, + }; + + vec![run_and_convert((globals, command, false))] +} + +/// Run cargo test on the selected package or all packages +/// +/// If no package is specified, loop through all packages +pub fn cargo_test<'c>( + globals: &Globals, + package: &'c PackageOpt, + backend: Backends, +) -> Vec<FinalRunResult<'c>> { + package + .packages() + .map(|p| { + let meta = TestMetadata::match_package(p, backend); + (globals, meta, false) + }) + .run_and_coalesce() +} + +/// Use mdbook to build the book +pub fn cargo_book<'c>( + globals: &Globals, + arguments: &'c Option<ExtraArguments>, +) -> Vec<FinalRunResult<'c>> { + vec![run_and_convert(( + globals, + CargoCommand::Book { + arguments: arguments.clone(), + }, + false, + ))] +} + +/// Run examples +/// +/// Supports updating the expected output via the overwrite argument +pub fn qemu_run_examples<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + backend: Backends, + examples: &'c [String], + overwrite: bool, +) -> Vec<FinalRunResult<'c>> { + let target = backend.to_target(); + let features = Some(target.and_features(backend.to_rtic_feature())); + + into_iter(examples) + .flat_map(|example| { + let target = target.into(); + let dir = Some(PathBuf::from("./rtic")); + + let cmd_build = CargoCommand::ExampleBuild { + cargoarg: &None, + example, + target, + features: features.clone(), + mode: BuildMode::Release, + dir: dir.clone(), + deny_warnings: globals.deny_warnings, + }; + + let cmd_qemu = CargoCommand::Qemu { + cargoarg, + example, + target, + features: features.clone(), + mode: BuildMode::Release, + dir, + deny_warnings: globals.deny_warnings, + }; + + into_iter([cmd_build, cmd_qemu]) + }) + .map(|cmd| (globals, cmd, overwrite)) + .run_and_coalesce() +} + +/// Check the binary sizes of examples +pub fn build_and_check_size<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + backend: Backends, + examples: &'c [String], + arguments: &'c Option<ExtraArguments>, +) -> Vec<FinalRunResult<'c>> { + let target = backend.to_target(); + let features = Some(target.and_features(backend.to_rtic_feature())); + + let runner = into_iter(examples) + .flat_map(|example| { + let target = target.into(); + + // Make sure the requested example(s) are built + let cmd_build = CargoCommand::ExampleBuild { + cargoarg: &Some("--quiet"), + example, + target, + features: features.clone(), + mode: BuildMode::Release, + dir: Some(PathBuf::from("./rtic")), + deny_warnings: globals.deny_warnings, + }; + + let cmd_size = CargoCommand::ExampleSize { + cargoarg, + example, + target, + features: features.clone(), + mode: BuildMode::Release, + arguments: arguments.clone(), + dir: Some(PathBuf::from("./rtic")), + deny_warnings: globals.deny_warnings, + }; + + [cmd_build, cmd_size] + }) + .map(|cmd| (globals, cmd, false)); + + runner.run_and_coalesce() +} + +fn run_command( + command: &CargoCommand, + stderr_mode: OutputMode, + print_command_success: bool, +) -> anyhow::Result<RunResult> { + log::info!("👟 {command}"); + + let mut process = Command::new(command.executable()); + + process + .args(command.args()) + .stdout(Stdio::piped()) + .stderr(stderr_mode); + + if let Some(dir) = command.chdir() { + process.current_dir(dir.canonicalize()?); + } + + if let Some((k, v)) = command.extra_env() { + process.env(k, v); + } + + let result = process.output()?; + + let exit_status = result.status; + let stderr = String::from_utf8(result.stderr).unwrap_or("Not displayable".into()); + let stdout = String::from_utf8(result.stdout).unwrap_or("Not displayable".into()); + + if command.print_stdout_intermediate() && exit_status.success() { + log::info!("\n{}", stdout); + } + + if print_command_success { + if exit_status.success() { + log::info!("✅ Success.") + } else { + log::error!("❌ Command failed. Run to completion for the summary."); + } + } + + Ok(RunResult { + exit_status, + stdout, + stderr, + }) +} diff --git a/xtask/src/run/data.rs b/xtask/src/run/data.rs new file mode 100644 index 0000000..eacd72c --- /dev/null +++ b/xtask/src/run/data.rs @@ -0,0 +1,87 @@ +use std::{ + ffi::OsString, + process::{ExitStatus, Stdio}, +}; + +use diffy::{create_patch, PatchFormatter}; + +use crate::cargo_command::CargoCommand; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum OutputMode { + PipedAndCollected, + Inherited, +} + +impl From<OutputMode> for Stdio { + fn from(value: OutputMode) -> Self { + match value { + OutputMode::PipedAndCollected => Stdio::piped(), + OutputMode::Inherited => Stdio::inherit(), + } + } +} + +#[derive(Debug, Clone)] +pub struct RunResult { + pub exit_status: ExitStatus, + pub stdout: String, + pub stderr: String, +} + +#[derive(Debug)] +pub enum FinalRunResult<'c> { + Success(CargoCommand<'c>, RunResult), + Failed(CargoCommand<'c>, RunResult), + CommandError(CargoCommand<'c>, anyhow::Error), +} + +#[derive(Debug)] +pub enum TestRunError { + FileCmpError { + expected: String, + got: String, + }, + FileError { + file: String, + }, + PathConversionError(OsString), + CommandError(RunResult), + #[allow(dead_code)] + IncompatibleCommand, +} + +impl core::fmt::Display for TestRunError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TestRunError::FileCmpError { expected, got } => { + let patch = create_patch(expected, got); + writeln!(f, "Differing output in files.\n")?; + let pf = PatchFormatter::new().with_color(); + writeln!(f, "{}", pf.fmt_patch(&patch))?; + write!( + f, + "See flag --overwrite-expected to create/update expected output." + ) + } + TestRunError::FileError { file } => { + write!(f, "File error on: {file}\nSee flag --overwrite-expected to create/update expected output.") + } + TestRunError::CommandError(e) => { + write!( + f, + "Command failed with exit status {}: {} {}", + e.exit_status, e.stdout, e.stderr + ) + } + TestRunError::PathConversionError(p) => { + write!(f, "Can't convert path from `OsString` to `String`: {p:?}") + } + TestRunError::IncompatibleCommand => { + write!(f, "Can't run that command in this context") + } + } + } +} + +impl std::error::Error for TestRunError {} diff --git a/xtask/src/run/iter.rs b/xtask/src/run/iter.rs new file mode 100644 index 0000000..d18ad49 --- /dev/null +++ b/xtask/src/run/iter.rs @@ -0,0 +1,48 @@ +use super::FinalRunResult; + +pub use iter::*; + +pub trait CoalescingRunner<'c> { + /// Run all the commands in this iterator, and coalesce the results into + /// one error (if any individual commands failed) + fn run_and_coalesce(self) -> Vec<FinalRunResult<'c>>; +} + +#[cfg(not(feature = "rayon"))] +mod iter { + use super::*; + use crate::{argument_parsing::Globals, cargo_command::*, run::run_and_convert}; + + pub fn into_iter<T: IntoIterator>(var: T) -> impl Iterator<Item = T::Item> { + var.into_iter() + } + + impl<'g, 'c, I> CoalescingRunner<'c> for I + where + I: Iterator<Item = (&'g Globals, CargoCommand<'c>, bool)>, + { + fn run_and_coalesce(self) -> Vec<FinalRunResult<'c>> { + self.map(run_and_convert).collect() + } + } +} + +#[cfg(feature = "rayon")] +mod iter { + use super::*; + use crate::{argument_parsing::Globals, cargo_command::*, run::run_and_convert}; + use rayon::prelude::*; + + pub fn into_iter<T: IntoParallelIterator>(var: T) -> impl ParallelIterator<Item = T::Item> { + var.into_par_iter() + } + + impl<'g, 'c, I> CoalescingRunner<'c> for I + where + I: ParallelIterator<Item = (&'g Globals, CargoCommand<'c>, bool)>, + { + fn run_and_coalesce(self) -> Vec<FinalRunResult<'c>> { + self.map(run_and_convert).collect() + } + } +} diff --git a/xtask/src/run/results.rs b/xtask/src/run/results.rs new file mode 100644 index 0000000..b64e7b1 --- /dev/null +++ b/xtask/src/run/results.rs @@ -0,0 +1,100 @@ +use log::{error, info, log, Level}; + +use crate::{argument_parsing::Globals, cargo_command::CargoCommand}; + +use super::data::FinalRunResult; + +const TARGET: &str = "xtask::results"; + +pub fn handle_results(globals: &Globals, results: Vec<FinalRunResult>) -> Result<(), ()> { + let errors = results.iter().filter_map(|r| { + if let FinalRunResult::Failed(c, r) = r { + Some((c, &r.stdout, &r.stderr)) + } else { + None + } + }); + + let successes = results.iter().filter_map(|r| { + if let FinalRunResult::Success(c, r) = r { + Some((c, &r.stdout, &r.stderr)) + } else { + None + } + }); + + let command_errors = results.iter().filter_map(|r| { + if let FinalRunResult::CommandError(c, e) = r { + Some((c, e)) + } else { + None + } + }); + + let log_stdout_stderr = |level: Level| { + move |(cmd, stdout, stderr): (&CargoCommand, &String, &String)| { + let cmd = cmd.as_cmd_string(); + if !stdout.is_empty() && !stderr.is_empty() { + log!( + target: TARGET, + level, + "\n{cmd}\nStdout:\n{stdout}\nStderr:\n{stderr}" + ); + } else if !stdout.is_empty() { + log!( + target: TARGET, + level, + "\n{cmd}\nStdout:\n{}", + stdout.trim_end() + ); + } else if !stderr.is_empty() { + log!( + target: TARGET, + level, + "\n{cmd}\nStderr:\n{}", + stderr.trim_end() + ); + } + } + }; + + successes.for_each(|(cmd, stdout, stderr)| { + if globals.verbose > 0 { + info!( + target: TARGET, + "✅ Success: {cmd}\n {}", + cmd.as_cmd_string() + ); + } else { + info!(target: TARGET, "✅ Success: {cmd}"); + } + + log_stdout_stderr(Level::Debug)((cmd, stdout, stderr)); + }); + + errors.clone().for_each(|(cmd, stdout, stderr)| { + error!( + target: TARGET, + "❌ Failed: {cmd}\n {}", + cmd.as_cmd_string() + ); + log_stdout_stderr(Level::Error)((cmd, stdout, stderr)); + }); + + command_errors.clone().for_each(|(cmd, error)| { + error!( + target: TARGET, + "❌ Failed: {cmd}\n {}\n{error}", + cmd.as_cmd_string() + ) + }); + + let ecount = errors.count() + command_errors.count(); + if ecount != 0 { + error!(target: TARGET, "{ecount} commands failed."); + Err(()) + } else { + info!(target: TARGET, "🚀🚀🚀 All tasks succeeded 🚀🚀🚀"); + Ok(()) + } +} |
