diff options
| author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2023-03-04 21:10:24 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-04 21:10:24 +0000 |
| commit | 7c7d6558f6d9c50fbb4d2487c98c9a5be15f2f7b (patch) | |
| tree | 80a47f0dc40059014e9448c4c2eb34c54dff45fe /xtask/src | |
| parent | 1c5db277e4161470136dbd2a11e914ff1d383581 (diff) | |
| parent | 98c5490d94950608d31cd5ad9dd260f2f853735c (diff) | |
Merge #694
694: RTIC 2 r=AfoHT a=korken89
Co-authored-by: Emil Fresk <emil.fresk@gmail.com>
Co-authored-by: Per Lindgren <per.lindgren@ltu.se>
Diffstat (limited to 'xtask/src')
| -rw-r--r-- | xtask/src/argument_parsing.rs | 260 | ||||
| -rw-r--r-- | xtask/src/build.rs | 2 | ||||
| -rw-r--r-- | xtask/src/cargo_commands.rs | 269 | ||||
| -rw-r--r-- | xtask/src/command.rs | 404 | ||||
| -rw-r--r-- | xtask/src/main.rs | 342 |
5 files changed, 1145 insertions, 132 deletions
diff --git a/xtask/src/argument_parsing.rs b/xtask/src/argument_parsing.rs new file mode 100644 index 0000000..8795213 --- /dev/null +++ b/xtask/src/argument_parsing.rs @@ -0,0 +1,260 @@ +use crate::{command::CargoCommand, ARMV6M, ARMV7M, ARMV8MBASE, ARMV8MMAIN, DEFAULT_FEATURES}; +use clap::{Args, Parser, Subcommand}; +use core::fmt; + +#[derive(clap::ValueEnum, Copy, Clone, Debug)] +pub enum Package { + Rtic, + RticCommon, + RticMacros, + RticMonotonics, + RticSync, + RticTime, +} + +impl fmt::Display for Package { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +impl Package { + pub fn name(&self) -> &str { + 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", + } + } +} + +pub struct TestMetadata {} + +impl TestMetadata { + pub fn match_package(package: Package, backend: Backends) -> CargoCommand<'static> { + match package { + Package::Rtic => { + let features = Some(format!( + "{},{},{}", + DEFAULT_FEATURES, + backend.to_rtic_feature(), + backend.to_rtic_uitest_feature(), + )); + CargoCommand::Test { + package: Some(package), + features, + test: Some("ui".to_owned()), + } + } + Package::RticMacros => CargoCommand::Test { + package: Some(package), + features: Some(backend.to_rtic_macros_feature().to_owned()), + test: None, + }, + Package::RticSync => CargoCommand::Test { + package: Some(package), + features: Some("testing".to_owned()), + test: None, + }, + Package::RticCommon => CargoCommand::Test { + package: Some(package), + features: Some("testing".to_owned()), + test: None, + }, + Package::RticMonotonics => CargoCommand::Test { + package: Some(package), + features: None, + test: None, + }, + Package::RticTime => CargoCommand::Test { + package: Some(package), + features: None, + test: None, + }, + } + } +} + +#[derive(clap::ValueEnum, Copy, Clone, Default, Debug)] +pub enum Backends { + Thumbv6, + #[default] + Thumbv7, + Thumbv8Base, + Thumbv8Main, +} + +impl Backends { + #[allow(clippy::wrong_self_convention)] + pub fn to_target(&self) -> &str { + match self { + Backends::Thumbv6 => ARMV6M, + Backends::Thumbv7 => ARMV7M, + Backends::Thumbv8Base => ARMV8MBASE, + Backends::Thumbv8Main => ARMV8MMAIN, + } + } + + #[allow(clippy::wrong_self_convention)] + pub fn to_rtic_feature(&self) -> &str { + match self { + Backends::Thumbv6 => "thumbv6-backend", + Backends::Thumbv7 => "thumbv7-backend", + Backends::Thumbv8Base => "thumbv8base-backend", + Backends::Thumbv8Main => "thumbv8main-backend", + } + } + #[allow(clippy::wrong_self_convention)] + pub fn to_rtic_macros_feature(&self) -> &str { + match self { + Backends::Thumbv6 | Backends::Thumbv8Base => "cortex-m-source-masking", + Backends::Thumbv7 | Backends::Thumbv8Main => "cortex-m-basepri", + } + } + #[allow(clippy::wrong_self_convention)] + pub fn to_rtic_uitest_feature(&self) -> &str { + match self { + Backends::Thumbv6 | Backends::Thumbv8Base => "rtic-uitestv6", + Backends::Thumbv7 | Backends::Thumbv8Main => "rtic-uitestv7", + } + } +} + +#[derive(Copy, Clone, Default, Debug)] +pub enum BuildOrCheck { + #[default] + Check, + Build, +} + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +/// RTIC xtask powered testing toolbox +pub struct Cli { + /// For which backend to build (defaults to thumbv7) + #[arg(value_enum, short, long)] + pub backend: Option<Backends>, + + /// List of comma separated examples to include, all others are excluded + /// + /// If omitted all examples are included + /// + /// Example: `cargo xtask --example complex,spawn,init` + /// would include complex, spawn and init + #[arg(short, long, group = "example_group")] + pub example: Option<String>, + + /// List of comma separated examples to exclude, all others are included + /// + /// If omitted all examples are included + /// + /// Example: `cargo xtask --excludeexample complex,spawn,init` + /// would exclude complex, spawn and init + #[arg(long, group = "example_group")] + pub exampleexclude: Option<String>, + + /// Enable more verbose output, repeat up to `-vvv` for even more + #[arg(short, long, action = clap::ArgAction::Count)] + pub verbose: u8, + + /// Subcommand selecting operation + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Debug, Subcommand)] +pub enum Commands { + /// Check formatting + FormatCheck(PackageOpt), + + /// Format code + Format(PackageOpt), + + /// Run clippy + Clippy(PackageOpt), + + /// Check all packages + Check(PackageOpt), + + /// Build all packages + Build(PackageOpt), + + /// Check all examples + ExampleCheck, + + /// Build all examples + ExampleBuild, + + /// Run `cargo size` on selected or all examples + /// + /// To pass options to `cargo size`, add `--` and then the following + /// arguments will be passed on + /// + /// Example: `cargo xtask size -- -A` + Size(Arg), + + /// Run examples in QEMU and compare against expected output + /// + /// Example runtime output is matched against `rtic/ci/expected/` + /// + /// Requires that an ARM target is selected + Qemu(QemuAndRun), + + /// Run examples through embedded-ci and compare against expected output + /// + /// unimplemented!() For now TODO, equal to Qemu + /// + /// Example runtime output is matched against `rtic/ci/expected/` + /// + /// Requires that an ARM target is selected + Run(QemuAndRun), + + /// Build docs + /// + /// To pass options to `cargo doc`, add `--` and then the following + /// arguments will be passed on + /// + /// Example: `cargo xtask doc -- --open` + Doc(Arg), + + /// Run tests + Test(PackageOpt), + + /// Build books with mdbook + Book(Arg), +} + +#[derive(Args, Debug)] +/// Restrict to package, or run on whole workspace +pub struct PackageOpt { + /// For which package/workspace member to operate + /// + /// If omitted, work on all + pub package: Option<Package>, +} + +#[derive(Args, Debug)] +pub struct QemuAndRun { + /// If expected output is missing or mismatching, recreate the file + /// + /// This overwrites only missing or mismatching + #[arg(long)] + pub overwrite_expected: bool, +} + +#[derive(Debug, Parser)] +pub struct Arg { + /// Options to pass to `cargo size` + #[command(subcommand)] + pub arguments: Option<ExtraArguments>, +} + +#[derive(Clone, Debug, PartialEq, Parser)] +pub enum ExtraArguments { + /// All remaining flags and options + #[command(external_subcommand)] + Other(Vec<String>), +} diff --git a/xtask/src/build.rs b/xtask/src/build.rs index 148a9fd..a11b4e0 100644 --- a/xtask/src/build.rs +++ b/xtask/src/build.rs @@ -2,7 +2,7 @@ use std::{fs, path::Path}; const HEX_BUILD_ROOT: &str = "ci/builds"; -/// make sure we're starting with a clean,but existing slate +/// Make sure we're starting with a clean, but existing slate pub fn init_build_dir() -> anyhow::Result<()> { if Path::new(HEX_BUILD_ROOT).exists() { fs::remove_dir_all(HEX_BUILD_ROOT) diff --git a/xtask/src/cargo_commands.rs b/xtask/src/cargo_commands.rs new file mode 100644 index 0000000..7ac7aea --- /dev/null +++ b/xtask/src/cargo_commands.rs @@ -0,0 +1,269 @@ +use crate::{ + argument_parsing::{Backends, BuildOrCheck, ExtraArguments, Package, PackageOpt, TestMetadata}, + command::{BuildMode, CargoCommand}, + command_parser, package_feature_extractor, DEFAULT_FEATURES, +}; +use log::error; +use rayon::prelude::*; + +/// Cargo command to either build or check +pub fn cargo( + operation: BuildOrCheck, + cargoarg: &Option<&str>, + package: &PackageOpt, + backend: Backends, +) -> anyhow::Result<()> { + let features = package_feature_extractor(package, backend); + + let command = match operation { + BuildOrCheck::Check => CargoCommand::Check { + cargoarg, + package: package.package, + target: backend.to_target(), + features, + mode: BuildMode::Release, + }, + BuildOrCheck::Build => CargoCommand::Build { + cargoarg, + package: package.package, + target: backend.to_target(), + features, + mode: BuildMode::Release, + }, + }; + command_parser(&command, false)?; + Ok(()) +} + +/// Cargo command to either build or check all examples +/// +/// The examples are in rtic/examples +pub fn cargo_example( + operation: BuildOrCheck, + cargoarg: &Option<&str>, + backend: Backends, + examples: &[String], +) -> anyhow::Result<()> { + examples.into_par_iter().for_each(|example| { + let features = Some(format!( + "{},{}", + DEFAULT_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, + }, + }; + + if let Err(err) = command_parser(&command, false) { + error!("{err}"); + } + }); + + Ok(()) +} + +/// Run cargo clippy on selected package +pub fn cargo_clippy( + cargoarg: &Option<&str>, + package: &PackageOpt, + backend: Backends, +) -> anyhow::Result<()> { + let features = package_feature_extractor(package, backend); + command_parser( + &CargoCommand::Clippy { + cargoarg, + package: package.package, + target: backend.to_target(), + features, + }, + false, + )?; + Ok(()) +} + +/// Run cargo fmt on selected package +pub fn cargo_format( + cargoarg: &Option<&str>, + package: &PackageOpt, + check_only: bool, +) -> anyhow::Result<()> { + command_parser( + &CargoCommand::Format { + cargoarg, + package: package.package, + check_only, + }, + false, + )?; + Ok(()) +} + +/// Run cargo doc +pub fn cargo_doc( + cargoarg: &Option<&str>, + backend: Backends, + arguments: &Option<ExtraArguments>, +) -> anyhow::Result<()> { + let features = Some(format!( + "{},{}", + DEFAULT_FEATURES, + backend.to_rtic_feature() + )); + + command_parser( + &CargoCommand::Doc { + cargoarg, + features, + arguments: arguments.clone(), + }, + false, + )?; + Ok(()) +} + +/// Run cargo test on the selcted package or all packages +/// +/// If no package is specified, loop through all packages +pub fn cargo_test(package: &PackageOpt, backend: Backends) -> anyhow::Result<()> { + if let Some(package) = package.package { + let cmd = TestMetadata::match_package(package, backend); + command_parser(&cmd, false)?; + } else { + // Iterate over all workspace packages + for package in [ + Package::Rtic, + Package::RticCommon, + Package::RticMacros, + Package::RticMonotonics, + Package::RticSync, + Package::RticTime, + ] { + let mut error_messages = vec![]; + let cmd = &TestMetadata::match_package(package, backend); + if let Err(err) = command_parser(cmd, false) { + error_messages.push(err); + } + + if !error_messages.is_empty() { + for err in error_messages { + error!("{err}"); + } + } + } + } + Ok(()) +} + +/// Use mdbook to build the book +pub fn cargo_book(arguments: &Option<ExtraArguments>) -> anyhow::Result<()> { + command_parser( + &CargoCommand::Book { + arguments: arguments.clone(), + }, + false, + )?; + Ok(()) +} + +/// Run examples +/// +/// Supports updating the expected output via the overwrite argument +pub fn run_test( + cargoarg: &Option<&str>, + backend: Backends, + examples: &[String], + overwrite: bool, +) -> anyhow::Result<()> { + examples.into_par_iter().for_each(|example| { + let cmd = CargoCommand::ExampleBuild { + cargoarg: &Some("--quiet"), + example, + target: backend.to_target(), + features: Some(format!( + "{},{}", + DEFAULT_FEATURES, + backend.to_rtic_feature() + )), + mode: BuildMode::Release, + }; + if let Err(err) = command_parser(&cmd, false) { + error!("{err}"); + } + + let cmd = CargoCommand::Qemu { + cargoarg, + example, + target: backend.to_target(), + features: Some(format!( + "{},{}", + DEFAULT_FEATURES, + backend.to_rtic_feature() + )), + mode: BuildMode::Release, + }; + + if let Err(err) = command_parser(&cmd, overwrite) { + error!("{err}"); + } + }); + + Ok(()) +} + +/// Check the binary sizes of examples +pub fn build_and_check_size( + cargoarg: &Option<&str>, + backend: Backends, + examples: &[String], + arguments: &Option<ExtraArguments>, +) -> anyhow::Result<()> { + examples.into_par_iter().for_each(|example| { + // Make sure the requested example(s) are built + let cmd = CargoCommand::ExampleBuild { + cargoarg: &Some("--quiet"), + example, + target: backend.to_target(), + features: Some(format!( + "{},{}", + DEFAULT_FEATURES, + backend.to_rtic_feature() + )), + mode: BuildMode::Release, + }; + if let Err(err) = command_parser(&cmd, false) { + error!("{err}"); + } + + let cmd = CargoCommand::ExampleSize { + cargoarg, + example, + target: backend.to_target(), + features: Some(format!( + "{},{}", + DEFAULT_FEATURES, + backend.to_rtic_feature() + )), + mode: BuildMode::Release, + arguments: arguments.clone(), + }; + if let Err(err) = command_parser(&cmd, false) { + error!("{err}"); + } + }); + + Ok(()) +} diff --git a/xtask/src/command.rs b/xtask/src/command.rs index 100888c..6e91a52 100644 --- a/xtask/src/command.rs +++ b/xtask/src/command.rs @@ -1,4 +1,4 @@ -use crate::{RunResult, TestRunError}; +use crate::{debug, ExtraArguments, Package, RunResult, TestRunError}; use core::fmt; use os_pipe::pipe; use std::{fs::File, io::Read, process::Command}; @@ -12,64 +12,364 @@ pub enum BuildMode { #[derive(Debug)] pub enum CargoCommand<'a> { + // For future embedded-ci + #[allow(dead_code)] Run { + cargoarg: &'a Option<&'a str>, example: &'a str, target: &'a str, - features: Option<&'a str>, + features: Option<String>, mode: BuildMode, }, - BuildAll { + Qemu { + cargoarg: &'a Option<&'a str>, + example: &'a str, + target: &'a str, + features: Option<String>, + mode: BuildMode, + }, + ExampleBuild { + cargoarg: &'a Option<&'a str>, + example: &'a str, target: &'a str, - features: Option<&'a str>, + features: Option<String>, mode: BuildMode, }, - // Size { - // example_paths: Vec<&'a Path>, - // }, - // Clean, + ExampleCheck { + cargoarg: &'a Option<&'a str>, + example: &'a str, + target: &'a str, + features: Option<String>, + mode: BuildMode, + }, + Build { + cargoarg: &'a Option<&'a str>, + package: Option<Package>, + target: &'a str, + features: Option<String>, + mode: BuildMode, + }, + Check { + cargoarg: &'a Option<&'a str>, + package: Option<Package>, + target: &'a str, + features: Option<String>, + mode: BuildMode, + }, + Clippy { + cargoarg: &'a Option<&'a str>, + package: Option<Package>, + target: &'a str, + 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: &'a str, + features: Option<String>, + mode: BuildMode, + arguments: Option<ExtraArguments>, + }, } impl<'a> CargoCommand<'a> { - fn name(&self) -> &str { + 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 { .. } => "run", - // CargoCommand::Size { example_paths: _ } => "rust-size", - CargoCommand::BuildAll { .. } => "build", + 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![ - self.name(), - "--example", - example, - "--target", - target, - "--features", - "test-critical-section", - ]; + 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]); - if let Some(feature_name) = features { - args.extend_from_slice(&["--features", feature_name]); + 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]); + + if let Some(feature) = features { + args.extend_from_slice(&["--features", feature]); } if let Some(flag) = mode.to_flag() { args.push(flag); } args } - CargoCommand::BuildAll { + CargoCommand::Build { + cargoarg, + package, target, features, mode, } => { - let mut args = vec![self.name(), "--examples", "--target", target]; + let mut args = vec!["+nightly"]; + if let Some(cargoarg) = cargoarg { + args.extend_from_slice(&[cargoarg]); + } + + args.extend_from_slice(&[self.command(), "--target", target]); + + 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]); + + 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]); + + 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]); if let Some(feature_name) = features { args.extend_from_slice(&["--features", feature_name]); @@ -77,25 +377,21 @@ impl<'a> CargoCommand<'a> { 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 - } // CargoCommand::Size { example_paths } => { - // example_paths.iter().map(|p| p.to_str().unwrap()).collect() - // } - } - } - - pub fn command(&self) -> &str { - match self { - // we need to cheat a little here: - // `cargo size` can't be ran on multiple files, so we're using `rust-size` instead – - // which isn't a command that starts wizh `cargo`. So we're sneakily swapping them out :) - // CargoCommand::Size { .. } => "rust-size", - _ => "cargo", + } } } } impl BuildMode { + #[allow(clippy::wrong_self_convention)] pub fn to_flag(&self) -> Option<&str> { match self { BuildMode::Release => Some("--release"), @@ -111,49 +407,55 @@ impl fmt::Display for BuildMode { BuildMode::Debug => "debug", }; - write!(f, "{}", cmd) + write!(f, "{cmd}") } } pub fn run_command(command: &CargoCommand) -> anyhow::Result<RunResult> { let (mut reader, writer) = pipe()?; - println!("👟 {} {}", command.command(), command.args().join(" ")); + let (mut error_reader, error_writer) = pipe()?; + debug!("👟 {} {}", command.executable(), command.args().join(" ")); - let mut handle = Command::new(command.command()) + let mut handle = Command::new(command.executable()) .args(command.args()) .stdout(writer) + .stderr(error_writer) .spawn()?; // retrieve output and clean up - let mut output = String::new(); - reader.read_to_string(&mut output)?; + let mut stdout = String::new(); + reader.read_to_string(&mut stdout)?; let exit_status = handle.wait()?; + let mut stderr = String::new(); + error_reader.read_to_string(&mut stderr)?; + Ok(RunResult { exit_status, - output, + stdout, + stderr, }) } -/// Check if `run` was sucessful. +/// 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: String) -> Result<(), TestRunError> { +pub fn run_successful(run: &RunResult, expected_output_file: &str) -> Result<(), TestRunError> { let mut file_handle = - File::open(expected_output_file.clone()).map_err(|_| TestRunError::FileError { - file: expected_output_file.clone(), + 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.clone(), + file: expected_output_file.to_owned(), })?; - if expected_output != run.output { + if expected_output != run.stdout { Err(TestRunError::FileCmpError { expected: expected_output.clone(), - got: run.output.clone(), + got: run.stdout.clone(), }) } else if !run.exit_status.success() { Err(TestRunError::CommandError(run.clone())) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 76ce04b..aed74eb 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,36 +1,51 @@ +mod argument_parsing; mod build; +mod cargo_commands; mod command; use anyhow::bail; +use argument_parsing::{ExtraArguments, Package}; +use clap::Parser; use core::fmt; +use diffy::{create_patch, PatchFormatter}; use std::{ error::Error, ffi::OsString, + fs::File, + io::prelude::*, path::{Path, PathBuf}, process, process::ExitStatus, str, }; -use structopt::StructOpt; + +use env_logger::Env; +use log::{debug, error, info, log_enabled, trace, Level}; use crate::{ + argument_parsing::{Backends, BuildOrCheck, Cli, Commands, PackageOpt}, build::init_build_dir, - command::{run_command, run_successful, BuildMode, CargoCommand}, + cargo_commands::{ + build_and_check_size, cargo, cargo_book, cargo_clippy, cargo_doc, cargo_example, + cargo_format, cargo_test, run_test, + }, + command::{run_command, run_successful, CargoCommand}, }; +// x86_64-unknown-linux-gnu +const _X86_64: &str = "x86_64-unknown-linux-gnu"; const ARMV6M: &str = "thumbv6m-none-eabi"; const ARMV7M: &str = "thumbv7m-none-eabi"; +const ARMV8MBASE: &str = "thumbv8m.base-none-eabi"; +const ARMV8MMAIN: &str = "thumbv8m.main-none-eabi"; -#[derive(Debug, StructOpt)] -struct Options { - #[structopt(short, long)] - target: String, -} +const DEFAULT_FEATURES: &str = "test-critical-section"; #[derive(Debug, Clone)] pub struct RunResult { exit_status: ExitStatus, - output: String, + stdout: String, + stderr: String, } #[derive(Debug)] @@ -41,31 +56,31 @@ pub enum TestRunError { 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 } => { - writeln!(f, "Differing output in files.")?; - writeln!(f, "")?; - writeln!(f, "Expected:")?; - writeln!(f, "{}", expected)?; - writeln!(f, "")?; - writeln!(f, "Got:")?; - write!(f, "{}", 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) + 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.output + e.exit_status, e.stdout ) } TestRunError::PathConversionError(p) => { - write!(f, "Can't convert path from `OsString` to `String`: {:?}", p) + write!(f, "Can't convert path from `OsString` to `String`: {p:?}") } TestRunError::IncompatibleCommand => { write!(f, "Can't run that command in this context") @@ -80,95 +95,262 @@ 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) let probably_running_from_repo_root = Path::new("./xtask").exists(); - if probably_running_from_repo_root == false { - bail!("xtasks can only be executed from the root of the `cortex-m-rtic` repository"); + if !probably_running_from_repo_root { + bail!("xtasks can only be executed from the root of the `rtic` repository"); } - let targets = [ARMV7M, ARMV6M]; - - let examples: Vec<_> = std::fs::read_dir("./examples")? - .filter_map(|path| { - path.map(|p| p.path().file_stem().unwrap().to_str().unwrap().to_string()) - .ok() - }) + let examples: Vec<_> = std::fs::read_dir("./rtic/examples")? + .filter_map(|p| p.ok()) + .map(|p| p.path()) + .filter(|p| p.display().to_string().ends_with(".rs")) + .map(|path| path.file_stem().unwrap().to_str().unwrap().to_string()) .collect(); - let opts = Options::from_args(); - let target = &opts.target; + let cli = Cli::parse(); - init_build_dir()?; + let env_logger_default_level = match cli.verbose { + 0 => Env::default().default_filter_or("error"), + 1 => Env::default().default_filter_or("info"), + 2 => Env::default().default_filter_or("debug"), + _ => Env::default().default_filter_or("trace"), + }; + env_logger::Builder::from_env(env_logger_default_level) + .format_module_path(false) + .format_timestamp(None) + .init(); - if target == "all" { - for t in targets { - run_test(t, &examples)?; - } - } else if targets.contains(&target.as_str()) { - run_test(&target, &examples)?; + trace!("default logging level: {0}", cli.verbose); + + let backend = if let Some(backend) = cli.backend { + backend } else { - eprintln!( - "The target you specified is not available. Available targets are:\ - \n{:?}\n\ - as well as `all` (testing on all of the above)", - targets - ); - process::exit(1); - } + Backends::default() + }; - Ok(()) -} + let example = cli.example; + let exampleexclude = cli.exampleexclude; + + let examples_to_run = { + let mut examples_to_run = examples.clone(); + + if let Some(example) = example { + examples_to_run = examples.clone(); + let examples_to_exclude = example.split(',').collect::<Vec<&str>>(); + // From the list of all examples, remove all not listed as included + for ex in examples_to_exclude { + examples_to_run.retain(|x| *x.as_str() == *ex); + } + }; -fn run_test(target: &str, examples: &[String]) -> anyhow::Result<()> { - arm_example(&CargoCommand::BuildAll { - target, - features: None, - mode: BuildMode::Release, - })?; - - for example in examples { - let cmd = CargoCommand::Run { - example, - target, - features: None, - mode: BuildMode::Release, + if let Some(example) = exampleexclude { + examples_to_run = examples.clone(); + let examples_to_exclude = example.split(',').collect::<Vec<&str>>(); + // From the list of all examples, remove all those listed as excluded + for ex in examples_to_exclude { + examples_to_run.retain(|x| *x.as_str() != *ex); + } }; - arm_example(&cmd)?; + if log_enabled!(Level::Trace) { + trace!("All examples:\n{examples:?} number: {}", examples.len()); + trace!( + "examples_to_run:\n{examples_to_run:?} number: {}", + examples_to_run.len() + ); + } + + if examples_to_run.is_empty() { + error!( + "\nThe example(s) you specified is not available. Available examples are:\ + \n{examples:#?}\n\ + By default if example flag is emitted, all examples are tested.", + ); + process::exit(exitcode::USAGE); + } else { + } + examples_to_run + }; + + init_build_dir()?; + #[allow(clippy::if_same_then_else)] + let cargologlevel = if log_enabled!(Level::Trace) { + Some("-v") + } else if log_enabled!(Level::Debug) { + None + } else if log_enabled!(Level::Info) { + None + } else if log_enabled!(Level::Warn) || log_enabled!(Level::Error) { + None + } else { + // Off case + Some("--quiet") + }; + + match cli.command { + Commands::FormatCheck(args) => { + info!("Running cargo fmt: {args:?}"); + let check_only = true; + cargo_format(&cargologlevel, &args, check_only)?; + } + Commands::Format(args) => { + info!("Running cargo fmt --check: {args:?}"); + let check_only = false; + cargo_format(&cargologlevel, &args, check_only)?; + } + Commands::Clippy(args) => { + info!("Running clippy on backend: {backend:?}"); + cargo_clippy(&cargologlevel, &args, backend)?; + } + Commands::Check(args) => { + info!("Checking on backend: {backend:?}"); + cargo(BuildOrCheck::Check, &cargologlevel, &args, backend)?; + } + Commands::Build(args) => { + info!("Building for backend: {backend:?}"); + cargo(BuildOrCheck::Build, &cargologlevel, &args, backend)?; + } + Commands::ExampleCheck => { + info!("Checking on backend: {backend:?}"); + cargo_example( + BuildOrCheck::Check, + &cargologlevel, + backend, + &examples_to_run, + )?; + } + Commands::ExampleBuild => { + info!("Building for backend: {backend:?}"); + cargo_example( + BuildOrCheck::Build, + &cargologlevel, + backend, + &examples_to_run, + )?; + } + Commands::Size(args) => { + // x86_64 target not valid + info!("Measuring for backend: {backend:?}"); + build_and_check_size(&cargologlevel, backend, &examples_to_run, &args.arguments)?; + } + Commands::Qemu(args) | Commands::Run(args) => { + // x86_64 target not valid + info!("Testing for backend: {backend:?}"); + run_test( + &cargologlevel, + backend, + &examples_to_run, + args.overwrite_expected, + )?; + } + Commands::Doc(args) => { + info!("Running cargo doc on backend: {backend:?}"); + cargo_doc(&cargologlevel, backend, &args.arguments)?; + } + Commands::Test(args) => { + info!("Running cargo test on backend: {backend:?}"); + cargo_test(&args, backend)?; + } + Commands::Book(args) => { + info!("Running mdbook"); + cargo_book(&args.arguments)?; + } } Ok(()) } +/// Get the features needed given the selected package +/// +/// Without package specified the features for RTIC are required +/// With only a single package which is not RTIC, no special +/// features are needed +fn package_feature_extractor(package: &PackageOpt, backend: Backends) -> Option<String> { + let default_features = Some(format!( + "{},{}", + DEFAULT_FEATURES, + backend.to_rtic_feature() + )); + if let Some(package) = package.package { + debug!("\nTesting package: {package}"); + match package { + Package::Rtic => default_features, + Package::RticMacros => Some(backend.to_rtic_macros_feature().to_owned()), + _ => None, + } + } else { + default_features + } +} + // run example binary `example` -fn arm_example(command: &CargoCommand) -> anyhow::Result<()> { +fn command_parser(command: &CargoCommand, overwrite: bool) -> anyhow::Result<()> { match *command { - CargoCommand::Run { example, .. } => { - let run_file = format!("{}.run", example); - let expected_output_file = ["ci", "expected", &run_file] + 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(|e| TestRunError::PathConversionError(e))?; + .map_err(TestRunError::PathConversionError)?; - // command is either build or run - let cargo_run_result = run_command(&command)?; - println!("{}", cargo_run_result.output); + // cargo run <..> + info!("Running example: {example}"); + let cargo_run_result = run_command(command)?; + info!("{}", cargo_run_result.stdout); - match &command { - CargoCommand::Run { .. } => { - run_successful(&cargo_run_result, expected_output_file)?; - } - _ => (), + // 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(()) } - CargoCommand::BuildAll { .. } => { - // command is either build or run - let cargo_run_result = run_command(&command)?; - println!("{}", cargo_run_result.output); + 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)?; + if let Some(exit_code) = cargo_result.exit_status.code() { + if exit_code != exitcode::OK { + error!("Exit code from command: {exit_code}"); + if !cargo_result.stdout.is_empty() { + info!("{}", cargo_result.stdout); + } + if !cargo_result.stderr.is_empty() { + error!("{}", cargo_result.stderr); + } + process::exit(exit_code); + } else { + if !cargo_result.stdout.is_empty() { + info!("{}", cargo_result.stdout); + } + if !cargo_result.stderr.is_empty() { + info!("{}", cargo_result.stderr); + } + } + } Ok(()) - } // _ => Err(anyhow::Error::new(TestRunError::IncompatibleCommand)), + } } } |
