aboutsummaryrefslogtreecommitdiff
path: root/xtask/src
diff options
context:
space:
mode:
Diffstat (limited to 'xtask/src')
-rw-r--r--xtask/src/argument_parsing.rs85
-rw-r--r--xtask/src/cargo_command.rs750
-rw-r--r--xtask/src/cargo_commands.rs345
-rw-r--r--xtask/src/command.rs779
-rw-r--r--xtask/src/main.rs156
-rw-r--r--xtask/src/run.rs501
-rw-r--r--xtask/src/run/data.rs87
-rw-r--r--xtask/src/run/iter.rs48
-rw-r--r--xtask/src/run/results.rs100
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(())
+ }
+}