aboutsummaryrefslogtreecommitdiff
path: root/xtask/src
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2023-03-04 21:10:24 +0000
committerGitHub <noreply@github.com>2023-03-04 21:10:24 +0000
commit7c7d6558f6d9c50fbb4d2487c98c9a5be15f2f7b (patch)
tree80a47f0dc40059014e9448c4c2eb34c54dff45fe /xtask/src
parent1c5db277e4161470136dbd2a11e914ff1d383581 (diff)
parent98c5490d94950608d31cd5ad9dd260f2f853735c (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.rs260
-rw-r--r--xtask/src/build.rs2
-rw-r--r--xtask/src/cargo_commands.rs269
-rw-r--r--xtask/src/command.rs404
-rw-r--r--xtask/src/main.rs342
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)),
+ }
}
}