diff --git a/xtask/src/argument_parsing.rs b/xtask/src/argument_parsing.rs index 3ee9e34ce5..d74ba69f8f 100644 --- a/xtask/src/argument_parsing.rs +++ b/xtask/src/argument_parsing.rs @@ -190,8 +190,8 @@ pub enum BuildOrCheck { #[derive(Parser, Clone)] pub struct Globals { - /// For which backend to build (defaults to thumbv7) - #[arg(value_enum, short, long, global = true)] + /// For which backend to build. + #[arg(value_enum, short, default_value = "thumbv7", long, global = true)] pub backend: Option, /// List of comma separated examples to include, all others are excluded @@ -300,6 +300,55 @@ pub enum Commands { /// Build books with mdbook Book(Arg), + + /// Check one or more usage examples. + /// + /// Usage examples are located in ./examples + UsageExamplesCheck(UsageExamples), + + /// Build one or more usage examples. + /// + /// Usage examples are located in ./examples + #[clap(alias = "./examples")] + UsageExampleBuild(UsageExamples), +} + +#[derive(Args, Clone, Debug)] +pub struct UsageExamples { + /// The usage examples to build. All usage examples are selected if this argument is not provided. + /// + /// Example: `rp2040_local_i2c_init,stm32f3_blinky`. + examples: Option, +} + +impl UsageExamples { + pub fn examples(&self) -> anyhow::Result> { + let usage_examples: Vec<_> = std::fs::read_dir("./examples")? + .filter_map(Result::ok) + .filter(|p| p.metadata().ok().map(|p| p.is_dir()).unwrap_or(false)) + .filter_map(|p| p.file_name().as_os_str().to_str().map(ToString::to_string)) + .collect(); + + let selected_examples: Option> = self + .examples + .clone() + .map(|s| s.split(",").map(ToString::to_string).collect()); + + if let Some(selected_examples) = selected_examples { + if let Some(unfound_example) = selected_examples + .iter() + .find(|e| !usage_examples.contains(e)) + { + Err(anyhow::anyhow!( + "Usage example {unfound_example} does not exist" + )) + } else { + Ok(selected_examples) + } + } else { + Ok(usage_examples) + } + } } #[derive(Args, Debug, Clone)] diff --git a/xtask/src/cargo_commands.rs b/xtask/src/cargo_commands.rs index 9cbdaefd75..9b07088c34 100644 --- a/xtask/src/cargo_commands.rs +++ b/xtask/src/cargo_commands.rs @@ -126,6 +126,33 @@ pub fn cargo<'c>( runner.run_and_coalesce() } +/// Cargo command to build a usage example. +/// +/// The usage examples are in examples/ +pub fn cargo_usage_example( + globals: &Globals, + operation: BuildOrCheck, + usage_examples: Vec, +) -> Vec> { + examples_iter(&usage_examples) + .map(|example| { + let path = format!("examples/{example}"); + + let command = match operation { + BuildOrCheck::Check => CargoCommand::CheckInDir { + mode: BuildMode::Release, + dir: path.into(), + }, + BuildOrCheck::Build => CargoCommand::BuildInDir { + mode: BuildMode::Release, + dir: path.into(), + }, + }; + (globals, command, false) + }) + .run_and_coalesce() +} + /// Cargo command to either build or check all examples /// /// The examples are in rtic/examples diff --git a/xtask/src/command.rs b/xtask/src/command.rs index b62724ab73..93de9cf65a 100644 --- a/xtask/src/command.rs +++ b/xtask/src/command.rs @@ -8,6 +8,7 @@ use core::fmt; use std::{ fs::File, io::Read, + path::PathBuf, process::{Command, Stdio}, }; @@ -111,6 +112,14 @@ pub enum CargoCommand<'a> { mode: BuildMode, arguments: Option, }, + CheckInDir { + mode: BuildMode, + dir: PathBuf, + }, + BuildInDir { + mode: BuildMode, + dir: PathBuf, + }, } impl core::fmt::Display for CargoCommand<'_> { @@ -211,6 +220,10 @@ impl core::fmt::Display for CargoCommand<'_> { details(target, mode, features, cargoarg) ) } + CargoCommand::BuildInDir { mode, dir } => { + let dir = dir.as_os_str().to_str().unwrap_or("Not displayable"); + write!(f, "Build {dir} ({mode})") + } CargoCommand::Check { cargoarg, package, @@ -225,6 +238,10 @@ impl core::fmt::Display for CargoCommand<'_> { details(target, mode, features, cargoarg) ) } + CargoCommand::CheckInDir { mode, dir } => { + let dir = dir.as_os_str().to_str().unwrap_or("Not displayable"); + write!(f, "Check {dir} ({mode})") + } CargoCommand::Clippy { cargoarg, package, @@ -316,11 +333,15 @@ impl<'a> CargoCommand<'a> { format!("{executable} {args}") } - fn command(&self) -> &str { + fn command(&self) -> &'static str { match self { CargoCommand::Run { .. } | CargoCommand::Qemu { .. } => "run", - CargoCommand::ExampleCheck { .. } | CargoCommand::Check { .. } => "check", - CargoCommand::ExampleBuild { .. } | CargoCommand::Build { .. } => "build", + CargoCommand::ExampleCheck { .. } + | CargoCommand::Check { .. } + | CargoCommand::CheckInDir { .. } => "check", + CargoCommand::ExampleBuild { .. } + | CargoCommand::Build { .. } + | CargoCommand::BuildInDir { .. } => "build", CargoCommand::ExampleSize { .. } => "size", CargoCommand::Clippy { .. } => "clippy", CargoCommand::Format { .. } => "fmt", @@ -329,7 +350,7 @@ impl<'a> CargoCommand<'a> { CargoCommand::Test { .. } => "test", } } - pub fn executable(&self) -> &str { + pub fn executable(&self) -> &'static str { match self { CargoCommand::Run { .. } | CargoCommand::Qemu { .. } @@ -341,7 +362,9 @@ impl<'a> CargoCommand<'a> { | CargoCommand::Clippy { .. } | CargoCommand::Format { .. } | CargoCommand::Test { .. } - | CargoCommand::Doc { .. } => "cargo", + | CargoCommand::Doc { .. } + | CargoCommand::CheckInDir { .. } + | CargoCommand::BuildInDir { .. } => "cargo", CargoCommand::Book { .. } => "mdbook", } } @@ -641,6 +664,34 @@ impl<'a> CargoCommand<'a> { } args } + CargoCommand::CheckInDir { mode, dir: _ } => { + let mut args = vec!["+nightly"]; + args.push(self.command()); + + if let Some(mode) = mode.to_flag() { + args.push(mode); + } + + args + } + CargoCommand::BuildInDir { mode, dir: _ } => { + let mut args = vec!["+nightly", self.command()]; + + if let Some(mode) = mode.to_flag() { + args.push(mode); + } + + args + } + } + } + + fn chdir(&self) -> Option<&PathBuf> { + match self { + CargoCommand::CheckInDir { dir, .. } | CargoCommand::BuildInDir { dir, .. } => { + Some(dir) + } + _ => None, } } } @@ -669,11 +720,18 @@ impl fmt::Display for BuildMode { pub fn run_command(command: &CargoCommand, stderr_mode: OutputMode) -> anyhow::Result { log::info!("👟 {command}"); - let result = Command::new(command.executable()) + let mut process = Command::new(command.executable()); + + process .args(command.args()) .stdout(Stdio::piped()) - .stderr(stderr_mode) - .output()?; + .stderr(stderr_mode); + + if let Some(dir) = command.chdir() { + process.current_dir(dir); + } + + let result = process.output()?; let exit_status = result.status; let stderr = String::from_utf8(result.stderr).unwrap_or("Not displayable".into()); @@ -759,15 +817,27 @@ pub fn handle_results(globals: &Globals, results: Vec) -> anyhow 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()); + let path = if let Some(dir) = cmd.chdir() { + let path = dir.as_os_str().to_str().unwrap_or("Not displayable"); + format!(" (in {path}") } else { - info!("✅ Success: {cmd}"); + format!("") + }; + + if globals.verbose > 0 { + info!("✅ Success:{path} {cmd}\n {}", cmd.as_cmd_string()); + } else { + info!("✅ Success:{path} {cmd}"); } }); errors.clone().for_each(|(cmd, _)| { - error!("❌ Failed: {cmd}\n {}", cmd.as_cmd_string()); + if let Some(dir) = cmd.chdir() { + let path = dir.as_os_str().to_str().unwrap_or("Not displayable"); + error!("❌ Failed: (in {path}) {cmd}\n {}", cmd.as_cmd_string()); + } else { + error!("❌ Failed: {cmd}\n {}", cmd.as_cmd_string()); + } }); let ecount = errors.count(); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 2bfe851f93..2b45f23793 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -23,10 +23,7 @@ use log::{error, info, log_enabled, trace, Level}; use crate::{ argument_parsing::{Backends, BuildOrCheck, Cli, Commands}, build::init_build_dir, - cargo_commands::{ - build_and_check_size, cargo, cargo_book, cargo_clippy, cargo_doc, cargo_example, - cargo_format, cargo_test, run_test, - }, + cargo_commands::*, command::{handle_results, run_command, run_successful, CargoCommand}, }; @@ -152,6 +149,12 @@ fn main() -> anyhow::Result<()> { trace!("default logging level: {0}", globals.verbose); + log::debug!( + "Stderr of child processes is inherited: {}", + globals.stderr_inherited + ); + log::debug!("Partial features: {}", globals.partial); + let backend = if let Some(backend) = globals.backend { backend } else { @@ -285,6 +288,14 @@ fn main() -> anyhow::Result<()> { info!("Running mdbook"); cargo_book(globals, &args.arguments) } + Commands::UsageExamplesCheck(examples) => { + info!("Checking usage examples"); + cargo_usage_example(globals, BuildOrCheck::Check, examples.examples()?) + } + Commands::UsageExampleBuild(examples) => { + info!("Building usage examples"); + cargo_usage_example(globals, BuildOrCheck::Build, examples.examples()?) + } }; handle_results(globals, final_run_results) @@ -347,7 +358,9 @@ fn command_parser( | CargoCommand::Doc { .. } | CargoCommand::Test { .. } | CargoCommand::Book { .. } - | CargoCommand::ExampleSize { .. } => { + | CargoCommand::ExampleSize { .. } + | CargoCommand::BuildInDir { .. } + | CargoCommand::CheckInDir { .. } => { let cargo_result = run_command(command, output_mode)?; Ok(cargo_result) }