734: QoL + fixes for `xtasks` r=korken89 a=datdenkikniet

Preferably merge this before #732

Draft till I get around to fixing "no package" -> "all packages" on `xtask build` (and probably others)

Co-authored-by: datdenkikniet <jcdra1@gmail.com>
This commit is contained in:
bors[bot] 2023-04-15 16:08:33 +00:00 committed by GitHub
commit ef8046b060
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 858 additions and 348 deletions

View file

@ -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"

View file

@ -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?

View file

@ -1,4 +1,12 @@
[workspace]
default-members = [
"rtic",
"rtic-sync",
"rtic-common",
"rtic-macros",
"rtic-monotonics",
"rtic-time",
]
members = [
"rtic",
"rtic-sync",

View file

@ -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"

View file

@ -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<Self> {
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<Option<String>> {
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<Backends>,
/// 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<String>,
/// 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<String>,
/// 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>,
package: Option<Package>,
}
#[derive(Args, Debug)]
impl PackageOpt {
#[cfg(not(feature = "rayon"))]
pub fn packages(&self) -> impl Iterator<Item = Package> {
self.package
.map(|p| vec![p])
.unwrap_or(Package::all())
.into_iter()
}
#[cfg(feature = "rayon")]
pub fn packages(&self) -> impl rayon::prelude::ParallelIterator<Item = Package> {
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<String>),
}
impl core::fmt::Display for ExtraArguments {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExtraArguments::Other(args) => {
write!(f, "{}", args.join(" "))
}
}
}
}

View file

@ -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<FinalRunResult<'c>>;
}
#[cfg(not(feature = "rayon"))]
mod iters {
use super::*;
pub fn examples_iter(examples: &[String]) -> impl Iterator<Item = &String> {
examples.into_iter()
}
impl<'g, 'c, I> CoalescingRunner<'c> for I
where
I: Iterator<Item = (&'g Globals, CargoCommand<'c>, bool)>,
{
fn run_and_coalesce(self) -> Vec<FinalRunResult<'c>> {
self.map(run_and_convert).collect()
}
}
}
#[cfg(feature = "rayon")]
mod iters {
use super::*;
pub fn examples_iter(examples: &[String]) -> impl ParallelIterator<Item = &String> {
examples.into_par_iter()
}
impl<'g, 'c, I> CoalescingRunner<'c> for I
where
I: ParallelIterator<Item = (&'g Globals, CargoCommand<'c>, bool)>,
{
fn run_and_coalesce(self) -> Vec<FinalRunResult<'c>> {
self.map(run_and_convert).collect()
}
}
}
/// Cargo command to either build or check
pub fn cargo<'c>(
globals: &Globals,
operation: BuildOrCheck,
cargoarg: &'c Option<&'c str>,
package: &'c PackageOpt,
backend: Backends,
) -> Vec<FinalRunResult<'c>> {
let runner = package
.packages()
.flat_map(|package| {
let target = backend.to_target();
let features = package.features(target, backend, globals.partial);
#[cfg(feature = "rayon")]
{
features.into_par_iter().map(move |f| (package, target, f))
}
#[cfg(not(feature = "rayon"))]
{
features.into_iter().map(move |f| (package, target, f))
}
})
.map(move |(package, target, features)| {
let command = match operation {
BuildOrCheck::Check => CargoCommand::Check {
cargoarg,
package: Some(package),
target,
features,
mode: BuildMode::Release,
},
BuildOrCheck::Build => CargoCommand::Build {
cargoarg,
package: Some(package),
target,
features,
mode: BuildMode::Release,
},
};
(globals, command, false)
});
runner.run_and_coalesce()
}
/// Cargo command to either build or check all examples
///
/// The examples are in rtic/examples
pub fn cargo_example(
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<FinalRunResult<'c>> {
let runner = examples_iter(examples).map(|example| {
let features = Some(backend.to_target().and_features(backend.to_rtic_feature()));
let command = match operation {
BuildOrCheck::Check => CargoCommand::ExampleCheck {
@ -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<FinalRunResult<'c>> {
let runner = package
.packages()
.flat_map(|package| {
let target = backend.to_target();
let features = package.features(target, backend, globals.partial);
#[cfg(feature = "rayon")]
{
features.into_par_iter().map(move |f| (package, target, f))
}
#[cfg(not(feature = "rayon"))]
{
features.into_iter().map(move |f| (package, target, f))
}
})
.map(move |(package, target, features)| {
(
globals,
CargoCommand::Clippy {
cargoarg,
package: Some(package),
target,
features,
},
false,
)
});
runner.run_and_coalesce()
}
/// Run cargo fmt on selected package
pub fn cargo_format(
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<FinalRunResult<'c>> {
let runner = package.packages().map(|p| {
(
globals,
CargoCommand::Format {
cargoarg,
package: Some(p),
check_only,
},
false,
)
});
runner.run_and_coalesce()
}
/// Run cargo doc
pub fn cargo_doc(
cargoarg: &Option<&str>,
pub fn cargo_doc<'c>(
globals: &Globals,
cargoarg: &'c Option<&'c str>,
backend: Backends,
arguments: &Option<ExtraArguments>,
) -> anyhow::Result<()> {
let features = Some(format!(
"{},{}",
DEFAULT_FEATURES,
backend.to_rtic_feature()
));
arguments: &'c Option<ExtraArguments>,
) -> Vec<FinalRunResult<'c>> {
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<FinalRunResult<'c>> {
package
.packages()
.map(|p| (globals, TestMetadata::match_package(p, backend), false))
.run_and_coalesce()
}
/// Use mdbook to build the book
pub fn cargo_book(arguments: &Option<ExtraArguments>) -> anyhow::Result<()> {
command_parser(
&CargoCommand::Book {
pub fn cargo_book<'c>(
globals: &Globals,
arguments: &'c Option<ExtraArguments>,
) -> Vec<FinalRunResult<'c>> {
vec![run_and_convert((
globals,
CargoCommand::Book {
arguments: arguments.clone(),
},
false,
)?;
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<FinalRunResult<'c>> {
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<ExtraArguments>,
) -> anyhow::Result<()> {
examples.into_par_iter().for_each(|example| {
examples: &'c [String],
arguments: &'c Option<ExtraArguments>,
) -> Vec<FinalRunResult<'c>> {
let target = backend.to_target();
let features = Some(target.and_features(backend.to_rtic_feature()));
let runner = examples_iter(examples).map(|example| {
// Make sure the requested example(s) are built
let cmd = CargoCommand::ExampleBuild {
cargoarg: &Some("--quiet"),
example,
target: 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()
}

View file

@ -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<OutputMode> for Stdio {
fn from(value: OutputMode) -> Self {
match value {
OutputMode::PipedAndCollected => Stdio::piped(),
OutputMode::Inherited => Stdio::inherit(),
}
}
}
#[derive(Debug)]
pub enum CargoCommand<'a> {
// For future embedded-ci
@ -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<String>,
mode: BuildMode,
},
Qemu {
cargoarg: &'a Option<&'a str>,
example: &'a str,
target: &'a str,
target: Target<'a>,
features: Option<String>,
mode: BuildMode,
},
ExampleBuild {
cargoarg: &'a Option<&'a str>,
example: &'a str,
target: &'a str,
target: Target<'a>,
features: Option<String>,
mode: BuildMode,
},
ExampleCheck {
cargoarg: &'a Option<&'a str>,
example: &'a str,
target: &'a str,
target: Target<'a>,
features: Option<String>,
mode: BuildMode,
},
Build {
cargoarg: &'a Option<&'a str>,
package: Option<Package>,
target: &'a str,
target: Target<'a>,
features: Option<String>,
mode: BuildMode,
},
Check {
cargoarg: &'a Option<&'a str>,
package: Option<Package>,
target: &'a str,
target: Target<'a>,
features: Option<String>,
mode: BuildMode,
},
Clippy {
cargoarg: &'a Option<&'a str>,
package: Option<Package>,
target: &'a str,
target: Target<'a>,
features: Option<String>,
},
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<String>,
mode: BuildMode,
arguments: Option<ExtraArguments>,
},
}
impl core::fmt::Display for CargoCommand<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let p = |p: &Option<Package>| {
if let Some(package) = p {
format!("package {package}")
} else {
format!("default package")
}
};
let feat = |f: &Option<String>| {
if let Some(features) = f {
format!("\"{features}\"")
} else {
format!("no features")
}
};
let carg = |f: &&Option<&str>| {
if let Some(cargoarg) = f {
format!("{cargoarg}")
} else {
format!("no cargo args")
}
};
let details = |target: &Target,
mode: &BuildMode,
features: &Option<String>,
cargoarg: &&Option<&str>| {
let feat = feat(features);
let carg = carg(cargoarg);
if cargoarg.is_some() {
format!("({target}, {mode}, {feat}, {carg})")
} else {
format!("({target}, {mode}, {feat})")
}
};
match self {
CargoCommand::Run {
cargoarg,
example,
target,
features,
mode,
} => write!(
f,
"Run example {example} {}",
details(target, mode, features, cargoarg)
),
CargoCommand::Qemu {
cargoarg,
example,
target,
features,
mode,
} => write!(
f,
"Run example {example} in QEMU {}",
details(target, mode, features, cargoarg)
),
CargoCommand::ExampleBuild {
cargoarg,
example,
target,
features,
mode,
} => write!(
f,
"Build example {example} {}",
details(target, mode, features, cargoarg)
),
CargoCommand::ExampleCheck {
cargoarg,
example,
target,
features,
mode,
} => write!(
f,
"Check example {example} {}",
details(target, mode, features, cargoarg)
),
CargoCommand::Build {
cargoarg,
package,
target,
features,
mode,
} => {
let package = p(package);
write!(
f,
"Build {package} {}",
details(target, mode, features, cargoarg)
)
}
CargoCommand::Check {
cargoarg,
package,
target,
features,
mode,
} => {
let package = p(package);
write!(
f,
"Check {package} {}",
details(target, mode, features, cargoarg)
)
}
CargoCommand::Clippy {
cargoarg,
package,
target,
features,
} => {
let package = p(package);
let features = feat(features);
let carg = carg(cargoarg);
if cargoarg.is_some() {
write!(f, "Clippy {package} ({target}, {features}, {carg})")
} else {
write!(f, "Clippy {package} ({target}, {features})")
}
}
CargoCommand::Format {
cargoarg,
package,
check_only,
} => {
let package = p(package);
let carg = carg(cargoarg);
let carg = if cargoarg.is_some() {
format!("(cargo args: {carg})")
} else {
format!("")
};
if *check_only {
write!(f, "Check format for {package} {carg}")
} else {
write!(f, "Format {package} {carg}")
}
}
CargoCommand::Doc {
cargoarg,
features,
arguments,
} => {
let feat = feat(features);
let carg = carg(cargoarg);
let arguments = arguments
.clone()
.map(|a| format!("{a}"))
.unwrap_or_else(|| "no extra arguments".into());
if cargoarg.is_some() {
write!(f, "Document ({feat}, {carg}, {arguments})")
} else {
write!(f, "Document ({feat}, {arguments})")
}
}
CargoCommand::Test {
package,
features,
test,
} => {
let p = p(package);
let test = test
.clone()
.map(|t| format!("test {t}"))
.unwrap_or("all tests".into());
let feat = feat(features);
write!(f, "Run {test} in {p} (features: {feat})")
}
CargoCommand::Book { arguments: _ } => write!(f, "Build the book"),
CargoCommand::ExampleSize {
cargoarg,
example,
target,
features,
mode,
arguments: _,
} => {
write!(
f,
"Compute size of example {example} {}",
details(target, mode, features, cargoarg)
)
}
}
}
}
impl<'a> CargoCommand<'a> {
pub fn as_cmd_string(&self) -> String {
let executable = self.executable();
let args = self.args().join(" ");
format!("{executable} {args}")
}
fn command(&self) -> &str {
match self {
CargoCommand::Run { .. } | CargoCommand::Qemu { .. } => "run",
@ -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<RunResult> {
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<RunResult> {
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<FinalRunResult>) -> anyhow::Result<()> {
let errors = results.iter().filter_map(|r| {
if let FinalRunResult::Failed(c, r) = r {
Some((c, r))
} else {
None
}
});
let successes = results.iter().filter_map(|r| {
if let FinalRunResult::Success(c, r) = r {
Some((c, r))
} else {
None
}
});
let log_stdout_stderr = |level: Level| {
move |(command, result): (&CargoCommand, &RunResult)| {
let stdout = &result.stdout;
let stderr = &result.stderr;
if !stdout.is_empty() && !stderr.is_empty() {
log::log!(
level,
"Output for \"{command}\"\nStdout:\n{stdout}\nStderr:\n{stderr}"
);
} else if !stdout.is_empty() {
log::log!(
level,
"Output for \"{command}\":\nStdout:\n{}",
stdout.trim_end()
);
} else if !stderr.is_empty() {
log::log!(
level,
"Output for \"{command}\"\nStderr:\n{}",
stderr.trim_end()
);
}
}
};
successes.clone().for_each(log_stdout_stderr(Level::Debug));
errors.clone().for_each(log_stdout_stderr(Level::Error));
successes.for_each(|(cmd, _)| {
if globals.verbose > 0 {
info!("✅ Success: {cmd}\n {}", cmd.as_cmd_string());
} else {
info!("✅ Success: {cmd}");
}
});
errors.clone().for_each(|(cmd, _)| {
error!("❌ Failed: {cmd}\n {}", cmd.as_cmd_string());
});
let ecount = errors.count();
if ecount != 0 {
Err(anyhow::anyhow!("{ecount} commands failed."))
} else {
Ok(())
}
}

View file

@ -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<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
}
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<RunResult> {
let output_mode = if glob.stderr_inherited {
OutputMode::Inherited
} else {
OutputMode::PipedAndCollected
};
match *command {
CargoCommand::Qemu { example, .. } | CargoCommand::Run { example, .. } => {
let run_file = format!("{example}.run");
@ -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)
}
}
}