diff --git a/.cargo/config.toml b/.cargo/config.toml index d70faef427..0a62466ad2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,6 @@ [alias] xtask = "run --package xtask --" +pxtask = "run --package xtask --features rayon --" [target.thumbv6m-none-eabi] runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel" @@ -10,4 +11,4 @@ runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semiho [target.'cfg(all(target_arch = "arm", target_os = "none"))'] rustflags = [ "-C", "link-arg=-Tlink.x", -] \ No newline at end of file +] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aadb0a9756..f2bec7dce4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,8 +31,8 @@ jobs: - name: Cache Dependencies uses: Swatinem/rust-cache@v2 - - name: cargo xtask format-check - run: cargo xtask --verbose format-check + - name: cargo xtask fmt + run: cargo xtask --verbose fmt -c # Compilation check check: @@ -251,7 +251,7 @@ jobs: tool: lychee - name: Remove cargo-config - run: rm -f .cargo/config + run: rm -f .cargo/config.toml - name: Build docs # TODO: Any difference between backends? diff --git a/Cargo.toml b/Cargo.toml index 888a6eecc9..6fccc1da54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,12 @@ [workspace] +default-members = [ + "rtic", + "rtic-sync", + "rtic-common", + "rtic-macros", + "rtic-monotonics", + "rtic-time", +] members = [ "rtic", "rtic-sync", diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 3c72bf1d3e..9e565fa470 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -6,10 +6,8 @@ publish = false [dependencies] anyhow = "1.0.43" -os_pipe = "1.1.2" clap = { version = "4", features = ["derive"] } -env_logger = "0.10.0" +pretty_env_logger = "0.4.0" log = "0.4.17" -rayon = "1.6.1" +rayon = { version = "1.6.1", optional = true } diffy = "0.3.0" -exitcode = "1.1.2" diff --git a/xtask/src/argument_parsing.rs b/xtask/src/argument_parsing.rs index c0538e204e..3ee9e34ce5 100644 --- a/xtask/src/argument_parsing.rs +++ b/xtask/src/argument_parsing.rs @@ -1,4 +1,4 @@ -use crate::{command::CargoCommand, ARMV6M, ARMV7M, ARMV8MBASE, ARMV8MMAIN, DEFAULT_FEATURES}; +use crate::{command::CargoCommand, Target, ARMV6M, ARMV7M, ARMV8MBASE, ARMV8MMAIN}; use clap::{Args, Parser, Subcommand}; use core::fmt; @@ -29,6 +29,64 @@ impl Package { Package::RticTime => "rtic-time", } } + + pub fn all() -> Vec { + vec![ + Self::Rtic, + Self::RticCommon, + Self::RticMacros, + Self::RticMonotonics, + Self::RticSync, + Self::RticTime, + ] + } + + /// 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 + pub fn features( + &self, + target: Target, + backend: Backends, + partial: bool, + ) -> Vec> { + match self { + Package::Rtic => vec![Some(target.and_features(backend.to_rtic_feature()))], + Package::RticMacros => { + vec![Some(backend.to_rtic_macros_feature().to_string())] + } + Package::RticMonotonics => { + let features = if partial { + &["cortex-m-systick", "rp2040", "nrf52840"][..] + } else { + &[ + "cortex-m-systick", + "cortex-m-systick,systick-100hz", + "cortex-m-systick,systick-10khz", + "rp2040", + "nrf52810", + "nrf52811", + "nrf52832", + "nrf52833", + "nrf52840", + "nrf5340-app", + "nrf5340-net", + "nrf9160", + ][..] + }; + + features + .into_iter() + .map(ToString::to_string) + .map(Some) + .chain(std::iter::once(None)) + .collect() + } + _ => vec![None], + } + } } pub struct TestMetadata {} @@ -37,12 +95,12 @@ impl TestMetadata { pub fn match_package(package: Package, backend: Backends) -> CargoCommand<'static> { match package { Package::Rtic => { - let features = Some(format!( - "{},{},{}", - DEFAULT_FEATURES, + let features = format!( + "{},{}", backend.to_rtic_feature(), - backend.to_rtic_uitest_feature(), - )); + backend.to_rtic_uitest_feature() + ); + let features = Some(backend.to_target().and_features(&features)); CargoCommand::Test { package: Some(package), features, @@ -89,7 +147,7 @@ pub enum Backends { impl Backends { #[allow(clippy::wrong_self_convention)] - pub fn to_target(&self) -> &str { + pub fn to_target(&self) -> Target<'static> { match self { Backends::Thumbv6 => ARMV6M, Backends::Thumbv7 => ARMV7M, @@ -99,7 +157,7 @@ impl Backends { } #[allow(clippy::wrong_self_convention)] - pub fn to_rtic_feature(&self) -> &str { + pub fn to_rtic_feature(&self) -> &'static str { match self { Backends::Thumbv6 => "thumbv6-backend", Backends::Thumbv7 => "thumbv7-backend", @@ -108,14 +166,14 @@ impl Backends { } } #[allow(clippy::wrong_self_convention)] - pub fn to_rtic_macros_feature(&self) -> &str { + pub fn to_rtic_macros_feature(&self) -> &'static 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 { + pub fn to_rtic_uitest_feature(&self) -> &'static str { match self { Backends::Thumbv6 | Backends::Thumbv8Base => "rtic-uitestv6", Backends::Thumbv7 | Backends::Thumbv8Main => "rtic-uitestv7", @@ -130,12 +188,10 @@ pub enum BuildOrCheck { Build, } -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -/// RTIC xtask powered testing toolbox -pub struct Cli { +#[derive(Parser, Clone)] +pub struct Globals { /// For which backend to build (defaults to thumbv7) - #[arg(value_enum, short, long)] + #[arg(value_enum, short, long, global = true)] pub backend: Option, /// List of comma separated examples to include, all others are excluded @@ -144,7 +200,7 @@ pub struct Cli { /// /// Example: `cargo xtask --example complex,spawn,init` /// would include complex, spawn and init - #[arg(short, long, group = "example_group")] + #[arg(short, long, group = "example_group", global = true)] pub example: Option, /// List of comma separated examples to exclude, all others are included @@ -153,25 +209,44 @@ pub struct Cli { /// /// Example: `cargo xtask --excludeexample complex,spawn,init` /// would exclude complex, spawn and init - #[arg(long, group = "example_group")] + #[arg(long, group = "example_group", global = true)] pub exampleexclude: Option, /// Enable more verbose output, repeat up to `-vvv` for even more - #[arg(short, long, action = clap::ArgAction::Count)] + #[arg(short, long, action = clap::ArgAction::Count, global = true)] pub verbose: u8, + /// Enable `stderr` inheritance on child processes. + /// + /// If this flag is enabled, the output of `stderr` produced by child + /// processes is printed directly to `stderr`. This will cause a lot of + /// clutter, but can make debugging long-running processes a lot easier. + #[arg(short, long, global = true)] + pub stderr_inherited: bool, + + /// Don't build/check/test all feature combinations that are available, only + /// a necessary subset. + #[arg(long, global = true)] + pub partial: bool, +} + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +/// RTIC xtask powered testing toolbox +pub struct Cli { + #[clap(flatten)] + pub globals: Globals, + /// Subcommand selecting operation #[command(subcommand)] pub command: Commands, } -#[derive(Debug, Subcommand)] +#[derive(Debug, Clone, Subcommand)] pub enum Commands { - /// Check formatting - FormatCheck(PackageOpt), - /// Format code - Format(PackageOpt), + #[clap(alias = "fmt")] + Format(FormatOpt), /// Run clippy Clippy(PackageOpt), @@ -227,16 +302,44 @@ pub enum Commands { Book(Arg), } -#[derive(Args, Debug)] +#[derive(Args, Debug, Clone)] +pub struct FormatOpt { + #[clap(flatten)] + pub package: PackageOpt, + /// Check-only, do not apply formatting fixes. + #[clap(short, long)] + pub check: bool, +} + +#[derive(Args, Debug, Clone)] /// 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: Option, } -#[derive(Args, Debug)] +impl PackageOpt { + #[cfg(not(feature = "rayon"))] + pub fn packages(&self) -> impl Iterator { + self.package + .map(|p| vec![p]) + .unwrap_or(Package::all()) + .into_iter() + } + + #[cfg(feature = "rayon")] + pub fn packages(&self) -> impl rayon::prelude::ParallelIterator { + use rayon::prelude::*; + self.package + .map(|p| vec![p]) + .unwrap_or(Package::all()) + .into_par_iter() + } +} + +#[derive(Args, Debug, Clone)] pub struct QemuAndRun { /// If expected output is missing or mismatching, recreate the file /// @@ -245,7 +348,7 @@ pub struct QemuAndRun { pub overwrite_expected: bool, } -#[derive(Debug, Parser)] +#[derive(Debug, Parser, Clone)] pub struct Arg { /// Options to pass to `cargo size` #[command(subcommand)] @@ -258,3 +361,13 @@ pub enum ExtraArguments { #[command(external_subcommand)] Other(Vec), } + +impl core::fmt::Display for ExtraArguments { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExtraArguments::Other(args) => { + write!(f, "{}", args.join(" ")) + } + } + } +} diff --git a/xtask/src/cargo_commands.rs b/xtask/src/cargo_commands.rs index 7ac7aea964..9cbdaefd75 100644 --- a/xtask/src/cargo_commands.rs +++ b/xtask/src/cargo_commands.rs @@ -1,55 +1,143 @@ use crate::{ - argument_parsing::{Backends, BuildOrCheck, ExtraArguments, Package, PackageOpt, TestMetadata}, + argument_parsing::{Backends, BuildOrCheck, ExtraArguments, Globals, PackageOpt, TestMetadata}, command::{BuildMode, CargoCommand}, - command_parser, package_feature_extractor, DEFAULT_FEATURES, + command_parser, RunResult, }; use log::error; + +#[cfg(feature = "rayon")] 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); +use iters::*; - 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(()) +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>; +} + +#[cfg(not(feature = "rayon"))] +mod iters { + use super::*; + + pub fn examples_iter(examples: &[String]) -> impl Iterator { + examples.into_iter() + } + + impl<'g, 'c, I> CoalescingRunner<'c> for I + where + I: Iterator, bool)>, + { + fn run_and_coalesce(self) -> Vec> { + self.map(run_and_convert).collect() + } + } +} + +#[cfg(feature = "rayon")] +mod iters { + use super::*; + + pub fn examples_iter(examples: &[String]) -> impl ParallelIterator { + examples.into_par_iter() + } + + impl<'g, 'c, I> CoalescingRunner<'c> for I + where + I: ParallelIterator, bool)>, + { + fn run_and_coalesce(self) -> Vec> { + 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> { + 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( +pub fn cargo_example<'c>( + globals: &Globals, operation: BuildOrCheck, - cargoarg: &Option<&str>, + cargoarg: &'c Option<&'c str>, backend: Backends, - examples: &[String], -) -> anyhow::Result<()> { - examples.into_par_iter().for_each(|example| { - let features = Some(format!( - "{},{}", - DEFAULT_FEATURES, - backend.to_rtic_feature() - )); + examples: &'c [String], +) -> Vec> { + 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 { @@ -67,184 +155,178 @@ pub fn cargo_example( mode: BuildMode::Release, }, }; - - if let Err(err) = command_parser(&command, false) { - error!("{err}"); - } + (globals, command, false) }); - - Ok(()) + runner.run_and_coalesce() } /// Run cargo clippy on selected package -pub fn cargo_clippy( - cargoarg: &Option<&str>, - package: &PackageOpt, +pub fn cargo_clippy<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + package: &'c 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(()) +) -> Vec> { + 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( - cargoarg: &Option<&str>, - package: &PackageOpt, +pub fn cargo_format<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, + package: &'c PackageOpt, check_only: bool, -) -> anyhow::Result<()> { - command_parser( - &CargoCommand::Format { - cargoarg, - package: package.package, - check_only, - }, - false, - )?; - Ok(()) +) -> Vec> { + 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( - cargoarg: &Option<&str>, +pub fn cargo_doc<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, backend: Backends, - arguments: &Option, -) -> anyhow::Result<()> { - let features = Some(format!( - "{},{}", - DEFAULT_FEATURES, - backend.to_rtic_feature() - )); + arguments: &'c Option, +) -> Vec> { + let features = Some(backend.to_target().and_features(backend.to_rtic_feature())); - command_parser( - &CargoCommand::Doc { - cargoarg, - features, - arguments: arguments.clone(), - }, - false, - )?; - Ok(()) + let command = CargoCommand::Doc { + cargoarg, + features, + arguments: arguments.clone(), + }; + + vec![run_and_convert((globals, command, false))] } -/// Run cargo test on the selcted package or all packages +/// Run cargo test on the selected 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(()) +pub fn cargo_test<'c>( + globals: &Globals, + package: &'c PackageOpt, + backend: Backends, +) -> Vec> { + package + .packages() + .map(|p| (globals, TestMetadata::match_package(p, backend), false)) + .run_and_coalesce() } /// Use mdbook to build the book -pub fn cargo_book(arguments: &Option) -> anyhow::Result<()> { - command_parser( - &CargoCommand::Book { +pub fn cargo_book<'c>( + globals: &Globals, + arguments: &'c Option, +) -> Vec> { + vec![run_and_convert(( + globals, + 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>, +pub fn run_test<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, backend: Backends, - examples: &[String], + examples: &'c [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}"); - } +) -> Vec> { + let target = backend.to_target(); + let features = Some(target.and_features(backend.to_rtic_feature())); - let cmd = CargoCommand::Qemu { - cargoarg, - example, - target: backend.to_target(), - features: Some(format!( - "{},{}", - DEFAULT_FEATURES, - backend.to_rtic_feature() - )), - mode: BuildMode::Release, - }; + 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(&cmd, overwrite) { - error!("{err}"); - } - }); + if let Err(err) = command_parser(globals, &cmd, false) { + error!("{err}"); + } - Ok(()) + 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( - cargoarg: &Option<&str>, +pub fn build_and_check_size<'c>( + globals: &Globals, + cargoarg: &'c Option<&'c str>, backend: Backends, - examples: &[String], - arguments: &Option, -) -> anyhow::Result<()> { - examples.into_par_iter().for_each(|example| { + examples: &'c [String], + arguments: &'c Option, +) -> Vec> { + 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: backend.to_target(), - features: Some(format!( - "{},{}", - DEFAULT_FEATURES, - backend.to_rtic_feature() - )), + target, + features: features.clone(), mode: BuildMode::Release, }; - if let Err(err) = command_parser(&cmd, false) { + if let Err(err) = command_parser(globals, &cmd, false) { error!("{err}"); } @@ -252,18 +334,12 @@ pub fn build_and_check_size( cargoarg, example, target: backend.to_target(), - features: Some(format!( - "{},{}", - DEFAULT_FEATURES, - backend.to_rtic_feature() - )), + features: features.clone(), mode: BuildMode::Release, arguments: arguments.clone(), }; - if let Err(err) = command_parser(&cmd, false) { - error!("{err}"); - } + (globals, cmd, false) }); - Ok(()) + runner.run_and_coalesce() } diff --git a/xtask/src/command.rs b/xtask/src/command.rs index 6e91a527a2..b62724ab73 100644 --- a/xtask/src/command.rs +++ b/xtask/src/command.rs @@ -1,7 +1,15 @@ -use crate::{debug, ExtraArguments, Package, RunResult, TestRunError}; +use log::{error, info, Level}; + +use crate::{ + argument_parsing::Globals, cargo_commands::FinalRunResult, ExtraArguments, Package, RunResult, + Target, TestRunError, +}; use core::fmt; -use os_pipe::pipe; -use std::{fs::File, io::Read, process::Command}; +use std::{ + fs::File, + io::Read, + process::{Command, Stdio}, +}; #[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq)] @@ -10,6 +18,21 @@ pub enum BuildMode { Debug, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum OutputMode { + PipedAndCollected, + Inherited, +} + +impl From 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 @@ -17,49 +40,49 @@ pub enum CargoCommand<'a> { Run { cargoarg: &'a Option<&'a str>, example: &'a str, - target: &'a str, + target: Target<'a>, features: Option, mode: BuildMode, }, Qemu { cargoarg: &'a Option<&'a str>, example: &'a str, - target: &'a str, + target: Target<'a>, features: Option, mode: BuildMode, }, ExampleBuild { cargoarg: &'a Option<&'a str>, example: &'a str, - target: &'a str, + target: Target<'a>, features: Option, mode: BuildMode, }, ExampleCheck { cargoarg: &'a Option<&'a str>, example: &'a str, - target: &'a str, + target: Target<'a>, features: Option, mode: BuildMode, }, Build { cargoarg: &'a Option<&'a str>, package: Option, - target: &'a str, + target: Target<'a>, features: Option, mode: BuildMode, }, Check { cargoarg: &'a Option<&'a str>, package: Option, - target: &'a str, + target: Target<'a>, features: Option, mode: BuildMode, }, Clippy { cargoarg: &'a Option<&'a str>, package: Option, - target: &'a str, + target: Target<'a>, features: Option, }, Format { @@ -83,14 +106,216 @@ pub enum CargoCommand<'a> { ExampleSize { cargoarg: &'a Option<&'a str>, example: &'a str, - target: &'a str, + target: Target<'a>, features: Option, mode: BuildMode, arguments: Option, }, } +impl core::fmt::Display for CargoCommand<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let p = |p: &Option| { + if let Some(package) = p { + format!("package {package}") + } else { + format!("default package") + } + }; + + let feat = |f: &Option| { + 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, + 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", @@ -135,7 +360,13 @@ impl<'a> CargoCommand<'a> { if let Some(cargoarg) = cargoarg { args.extend_from_slice(&[cargoarg]); } - args.extend_from_slice(&[self.command(), "--example", example, "--target", target]); + args.extend_from_slice(&[ + self.command(), + "--example", + example, + "--target", + target.triple(), + ]); if let Some(feature) = features { args.extend_from_slice(&["--features", feature]); @@ -156,7 +387,13 @@ impl<'a> CargoCommand<'a> { if let Some(cargoarg) = cargoarg { args.extend_from_slice(&[cargoarg]); } - args.extend_from_slice(&[self.command(), "--example", example, "--target", target]); + args.extend_from_slice(&[ + self.command(), + "--example", + example, + "--target", + target.triple(), + ]); if let Some(feature) = features { args.extend_from_slice(&["--features", feature]); @@ -178,7 +415,7 @@ impl<'a> CargoCommand<'a> { args.extend_from_slice(&[cargoarg]); } - args.extend_from_slice(&[self.command(), "--target", target]); + args.extend_from_slice(&[self.command(), "--target", target.triple()]); if let Some(package) = package { args.extend_from_slice(&["--package", package.name()]); @@ -326,7 +563,13 @@ impl<'a> CargoCommand<'a> { if let Some(cargoarg) = cargoarg { args.extend_from_slice(&[cargoarg]); } - args.extend_from_slice(&[self.command(), "--example", example, "--target", target]); + args.extend_from_slice(&[ + self.command(), + "--example", + example, + "--target", + target.triple(), + ]); if let Some(feature) = features { args.extend_from_slice(&["--features", feature]); @@ -347,7 +590,13 @@ impl<'a> CargoCommand<'a> { if let Some(cargoarg) = cargoarg { args.extend_from_slice(&[cargoarg]); } - args.extend_from_slice(&[self.command(), "--example", example, "--target", target]); + args.extend_from_slice(&[ + self.command(), + "--example", + example, + "--target", + target.triple(), + ]); if let Some(feature) = features { args.extend_from_slice(&["--features", feature]); @@ -369,7 +618,13 @@ impl<'a> CargoCommand<'a> { if let Some(cargoarg) = cargoarg { args.extend_from_slice(&[cargoarg]); } - args.extend_from_slice(&[self.command(), "--example", example, "--target", target]); + args.extend_from_slice(&[ + self.command(), + "--example", + example, + "--target", + target.triple(), + ]); if let Some(feature_name) = features { args.extend_from_slice(&["--features", feature_name]); @@ -411,24 +666,18 @@ impl fmt::Display for BuildMode { } } -pub fn run_command(command: &CargoCommand) -> anyhow::Result { - let (mut reader, writer) = pipe()?; - let (mut error_reader, error_writer) = pipe()?; - debug!("👟 {} {}", command.executable(), command.args().join(" ")); +pub fn run_command(command: &CargoCommand, stderr_mode: OutputMode) -> anyhow::Result { + log::info!("👟 {command}"); - let mut handle = Command::new(command.executable()) + let result = Command::new(command.executable()) .args(command.args()) - .stdout(writer) - .stderr(error_writer) - .spawn()?; + .stdout(Stdio::piped()) + .stderr(stderr_mode) + .output()?; - // retrieve output and clean up - 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)?; + 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, @@ -463,3 +712,68 @@ pub fn run_successful(run: &RunResult, expected_output_file: &str) -> Result<(), Ok(()) } } + +pub fn handle_results(globals: &Globals, results: Vec) -> 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 a7fd1d3229..2bfe851f93 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,9 +3,9 @@ mod build; mod cargo_commands; mod command; -use anyhow::bail; -use argument_parsing::{ExtraArguments, Package}; +use argument_parsing::{ExtraArguments, Globals, Package}; use clap::Parser; +use command::OutputMode; use core::fmt; use diffy::{create_patch, PatchFormatter}; use std::{ @@ -14,32 +14,60 @@ use std::{ fs::File, io::prelude::*, path::{Path, PathBuf}, - process, process::ExitStatus, str, }; -use env_logger::Env; -use log::{debug, error, info, log_enabled, trace, Level}; +use log::{error, info, log_enabled, trace, Level}; use crate::{ - argument_parsing::{Backends, BuildOrCheck, Cli, Commands, PackageOpt}, + 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::{run_command, run_successful, CargoCommand}, + command::{handle_results, 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, Clone, Copy)] +pub struct Target<'a> { + triple: &'a str, + has_std: bool, +} -const DEFAULT_FEATURES: &str = "test-critical-section"; +impl<'a> Target<'a> { + const DEFAULT_FEATURES: &str = "test-critical-section"; + + pub const fn new(triple: &'a str, has_std: bool) -> Self { + Self { triple, has_std } + } + + pub fn triple(&self) -> &str { + self.triple + } + + pub fn has_std(&self) -> bool { + self.has_std + } + + pub fn and_features(&self, features: &str) -> String { + format!("{},{}", Self::DEFAULT_FEATURES, features) + } +} + +impl core::fmt::Display for Target<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.triple) + } +} + +// x86_64-unknown-linux-gnu +const _X86_64: Target = Target::new("x86_64-unknown-linux-gnu", true); +const ARMV6M: Target = Target::new("thumbv6m-none-eabi", false); +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 { @@ -96,7 +124,9 @@ fn main() -> anyhow::Result<()> { // 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 { - bail!("xtasks can only be executed from the root of the `rtic` repository"); + return Err(anyhow::anyhow!( + "xtasks can only be executed from the root of the `rtic` repository" + )); } let examples: Vec<_> = std::fs::read_dir("./rtic/examples")? @@ -108,26 +138,28 @@ fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - let env_logger_default_level = match cli.verbose { - 0 => Env::default().default_filter_or("info"), - 1 => Env::default().default_filter_or("debug"), - _ => Env::default().default_filter_or("trace"), + let globals = &cli.globals; + + let env_logger_default_level = match globals.verbose { + 0 => "info", + 1 => "debug", + _ => "trace", }; - env_logger::Builder::from_env(env_logger_default_level) - .format_module_path(false) - .format_timestamp(None) + + pretty_env_logger::formatted_builder() + .parse_filters(&std::env::var("RUST_LOG").unwrap_or(env_logger_default_level.into())) .init(); - trace!("default logging level: {0}", cli.verbose); + trace!("default logging level: {0}", globals.verbose); - let backend = if let Some(backend) = cli.backend { + let backend = if let Some(backend) = globals.backend { backend } else { Backends::default() }; - let example = cli.example; - let exampleexclude = cli.exampleexclude; + let example = globals.example.clone(); + let exampleexclude = globals.exampleexclude.clone(); let examples_to_run = { let mut examples_to_run = examples.clone(); @@ -164,10 +196,10 @@ fn main() -> anyhow::Result<()> { \n{examples:#?}\n\ By default if example flag is emitted, all examples are tested.", ); - process::exit(exitcode::USAGE); + return Err(anyhow::anyhow!("Incorrect usage")); } else { + examples_to_run } - examples_to_run }; init_build_dir()?; @@ -185,104 +217,91 @@ fn main() -> anyhow::Result<()> { Some("--quiet") }; - match cli.command { - Commands::FormatCheck(args) => { - info!("Running cargo fmt --check: {args:?}"); - let check_only = true; - cargo_format(&cargologlevel, &args, check_only)?; - } - Commands::Format(args) => { - info!("Running cargo fmt: {args:?}"); - let check_only = false; - cargo_format(&cargologlevel, &args, check_only)?; - } + let final_run_results = match &cli.command { + Commands::Format(args) => cargo_format(globals, &cargologlevel, &args.package, args.check), Commands::Clippy(args) => { info!("Running clippy on backend: {backend:?}"); - cargo_clippy(&cargologlevel, &args, backend)?; + cargo_clippy(globals, &cargologlevel, &args, backend) } Commands::Check(args) => { info!("Checking on backend: {backend:?}"); - cargo(BuildOrCheck::Check, &cargologlevel, &args, backend)?; + cargo(globals, BuildOrCheck::Check, &cargologlevel, &args, backend) } Commands::Build(args) => { info!("Building for backend: {backend:?}"); - cargo(BuildOrCheck::Build, &cargologlevel, &args, backend)?; + cargo(globals, BuildOrCheck::Build, &cargologlevel, &args, backend) } Commands::ExampleCheck => { info!("Checking on backend: {backend:?}"); cargo_example( + globals, BuildOrCheck::Check, &cargologlevel, backend, &examples_to_run, - )?; + ) } Commands::ExampleBuild => { info!("Building for backend: {backend:?}"); cargo_example( + globals, 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)?; + build_and_check_size( + globals, + &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( + globals, &cargologlevel, backend, &examples_to_run, args.overwrite_expected, - )?; + ) } Commands::Doc(args) => { info!("Running cargo doc on backend: {backend:?}"); - cargo_doc(&cargologlevel, backend, &args.arguments)?; + cargo_doc(globals, &cargologlevel, backend, &args.arguments) } Commands::Test(args) => { info!("Running cargo test on backend: {backend:?}"); - cargo_test(&args, backend)?; + cargo_test(globals, &args, backend) } Commands::Book(args) => { info!("Running mdbook"); - cargo_book(&args.arguments)?; + cargo_book(globals, &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 { - 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 - } + handle_results(globals, final_run_results) } // run example binary `example` -fn command_parser(command: &CargoCommand, overwrite: bool) -> anyhow::Result<()> { +fn command_parser( + glob: &Globals, + command: &CargoCommand, + overwrite: bool, +) -> anyhow::Result { + 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"); @@ -295,7 +314,7 @@ fn command_parser(command: &CargoCommand, overwrite: bool) -> anyhow::Result<()> // cargo run <..> info!("Running example: {example}"); - let cargo_run_result = run_command(command)?; + 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 @@ -315,8 +334,9 @@ fn command_parser(command: &CargoCommand, overwrite: bool) -> anyhow::Result<()> }; } else { run_successful(&cargo_run_result, &expected_output_file)?; - } - Ok(()) + }; + + Ok(cargo_run_result) } CargoCommand::Format { .. } | CargoCommand::ExampleCheck { .. } @@ -328,28 +348,8 @@ fn command_parser(command: &CargoCommand, overwrite: bool) -> anyhow::Result<()> | 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(()) + let cargo_result = run_command(command, output_mode)?; + Ok(cargo_result) } } }