diff options
Diffstat (limited to 'xtask/src')
| -rw-r--r-- | xtask/src/build.rs | 13 | ||||
| -rw-r--r-- | xtask/src/command.rs | 201 | ||||
| -rw-r--r-- | xtask/src/main.rs | 240 |
3 files changed, 454 insertions, 0 deletions
diff --git a/xtask/src/build.rs b/xtask/src/build.rs new file mode 100644 index 0000000..a11b4e0 --- /dev/null +++ b/xtask/src/build.rs @@ -0,0 +1,13 @@ +use std::{fs, path::Path}; + +const HEX_BUILD_ROOT: &str = "ci/builds"; + +/// 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) + .map_err(|_| anyhow::anyhow!("Could not clear out directory: {}", HEX_BUILD_ROOT))?; + } + fs::create_dir_all(HEX_BUILD_ROOT) + .map_err(|_| anyhow::anyhow!("Could not create directory: {}", HEX_BUILD_ROOT)) +} diff --git a/xtask/src/command.rs b/xtask/src/command.rs new file mode 100644 index 0000000..6be1463 --- /dev/null +++ b/xtask/src/command.rs @@ -0,0 +1,201 @@ +use crate::Sizearguments; +use crate::{RunResult, TestRunError}; +use core::fmt; +use os_pipe::pipe; +use std::{fs::File, io::Read, process::Command}; + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BuildMode { + Release, + Debug, +} + +#[derive(Debug)] +pub enum CargoCommand<'a> { + Run { + example: &'a str, + target: &'a str, + features: Option<&'a str>, + mode: BuildMode, + }, + BuildAll { + target: &'a str, + features: Option<&'a str>, + mode: BuildMode, + }, + Size { + example: &'a str, + target: &'a str, + features: Option<&'a str>, + mode: BuildMode, + arguments: Option<Sizearguments>, + }, +} + +impl<'a> CargoCommand<'a> { + fn name(&self) -> &str { + match self { + CargoCommand::Run { .. } => "run", + CargoCommand::Size { .. } => "size", + CargoCommand::BuildAll { .. } => "build", + } + } + + pub fn args(&self) -> Vec<&str> { + match self { + CargoCommand::Run { + example, + target, + features, + mode, + } => { + let mut args = vec![ + "+nightly", + self.name(), + "--example", + example, + "--target", + target, + "--features", + "test-critical-section", + ]; + + if let Some(feature_name) = features { + args.extend_from_slice(&["--features", feature_name]); + } + if let Some(flag) = mode.to_flag() { + args.push(flag); + } + args + } + CargoCommand::BuildAll { + target, + features, + mode, + } => { + let mut args = vec![ + "+nightly", + self.name(), + "--examples", + "--target", + target, + "--features", + "test-critical-section", + ]; + + if let Some(feature_name) = features { + args.extend_from_slice(&["--features", feature_name]); + } + if let Some(flag) = mode.to_flag() { + args.push(flag); + } + args + } + CargoCommand::Size { + example, + target, + features, + mode, + arguments, + } => { + let mut args = vec![ + "+nightly", + self.name(), + "--example", + example, + "--target", + target, + "--features", + "test-critical-section", + ]; + if let Some(feature_name) = features { + args.extend_from_slice(&["--features", feature_name]); + } + if let Some(flag) = mode.to_flag() { + args.push(flag); + } + if let Some(Sizearguments::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 + } + } + } + + pub fn command(&self) -> &str { + "cargo" + } +} + +impl BuildMode { + #[allow(clippy::wrong_self_convention)] + pub fn to_flag(&self) -> Option<&str> { + match self { + BuildMode::Release => Some("--release"), + BuildMode::Debug => None, + } + } +} + +impl fmt::Display for BuildMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cmd = match self { + BuildMode::Release => "release", + BuildMode::Debug => "debug", + }; + + 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 handle = Command::new(command.command()) + .args(command.args()) + .stdout(writer) + .spawn()?; + + // retrieve output and clean up + let mut output = String::new(); + reader.read_to_string(&mut output)?; + let exit_status = handle.wait()?; + + Ok(RunResult { + exit_status, + output, + }) +} + +/// 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> { + let mut file_handle = + File::open(expected_output_file.clone()).map_err(|_| TestRunError::FileError { + file: expected_output_file.clone(), + })?; + let mut expected_output = String::new(); + file_handle + .read_to_string(&mut expected_output) + .map_err(|_| TestRunError::FileError { + file: expected_output_file.clone(), + })?; + + if expected_output != run.output { + Err(TestRunError::FileCmpError { + expected: expected_output.clone(), + got: run.output.clone(), + }) + } else if !run.exit_status.success() { + Err(TestRunError::CommandError(run.clone())) + } else { + Ok(()) + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..7c0ed20 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,240 @@ +mod build; +mod command; + +use anyhow::bail; +use core::fmt; +use std::{ + error::Error, + ffi::OsString, + path::{Path, PathBuf}, + process, + process::ExitStatus, + str, +}; +use structopt::StructOpt; + +use crate::{ + build::init_build_dir, + command::{run_command, run_successful, BuildMode, CargoCommand}, +}; + +const ARMV6M: &str = "thumbv6m-none-eabi"; +const ARMV7M: &str = "thumbv7m-none-eabi"; + +#[derive(Debug, StructOpt)] +struct Options { + /// For which ARM target to build: v7 or v6 + /// + /// The permissible targets are: + /// * all + /// + /// * thumbv6m-none-eabi + /// + /// * thumbv7m-none-eabi + #[structopt(short, long)] + target: String, + /// Example to run, by default all examples are run + /// + /// Example: `cargo xtask --target <..> --example complex` + #[structopt(short, long)] + example: Option<String>, + /// Enables also running `cargo size` on the selected examples + /// + /// To pass options to `cargo size`, add `--` and then the following + /// arguments will be passed on + /// + /// Example: `cargo xtask --target <..> -s -- -A` + #[structopt(short, long)] + size: bool, + /// Options to pass to `cargo size` + #[structopt(subcommand)] + sizearguments: Option<Sizearguments>, +} + +#[derive(Clone, Debug, PartialEq, StructOpt)] +pub enum Sizearguments { + // `external_subcommand` tells structopt to put + // all the extra arguments into this Vec + #[structopt(external_subcommand)] + Other(Vec<String>), +} + +#[derive(Debug, Clone)] +pub struct RunResult { + exit_status: ExitStatus, + output: String, +} + +#[derive(Debug)] +pub enum TestRunError { + FileCmpError { expected: String, got: String }, + FileError { file: String }, + PathConversionError(OsString), + 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.\n")?; + writeln!(f, "Expected:")?; + writeln!(f, "{expected}\n")?; + writeln!(f, "Got:")?; + write!(f, "{got}") + } + TestRunError::FileError { file } => { + write!(f, "File error on: {file}") + } + TestRunError::CommandError(e) => { + write!( + f, + "Command failed with exit status {}: {}", + e.exit_status, e.output + ) + } + TestRunError::PathConversionError(p) => { + write!(f, "Can't convert path from `OsString` to `String`: {p:?}") + } + TestRunError::IncompatibleCommand => { + write!(f, "Can't run that command in this context") + } + } + } +} + +impl Error for TestRunError {} + +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 { + bail!("xtasks can only be executed from the root of the `rtic` repository"); + } + + let targets = [ARMV7M, ARMV6M]; + + let mut 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(); + + println!("examples: {examples:?}"); + + let opts = Options::from_args(); + let target = &opts.target; + let check_size = opts.size; + let size_arguments = &opts.sizearguments; + let example = opts.example; + + if let Some(example) = example { + if examples.contains(&example) { + println!("\nTesting example: {example}"); + // If we managed to filter, set the examples to test to only this one + examples = vec![example] + } else { + eprintln!( + "\nThe example you specified is not available. Available examples are:\ + \n{examples:#?}\n\ + By default all examples are tested.", + ); + process::exit(1); + } + } + init_build_dir()?; + + if target == "all" { + for t in targets { + run_test(t, &examples, check_size, size_arguments)?; + } + } else if targets.contains(&target.as_str()) { + run_test(target, &examples, check_size, size_arguments)?; + } else { + eprintln!( + "The target you specified is not available. Available targets are:\ + \n{targets:?}\n\ + as well as `all` (testing on all of the above)", + ); + process::exit(1); + } + + Ok(()) +} + +fn run_test( + target: &str, + examples: &[String], + check_size: bool, + size_arguments: &Option<Sizearguments>, +) -> 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, + }; + + arm_example(&cmd)?; + } + if check_size { + for example in examples { + arm_example(&CargoCommand::Size { + example, + target, + features: None, + mode: BuildMode::Release, + arguments: size_arguments.clone(), + })?; + } + } + + Ok(()) +} + +// run example binary `example` +fn arm_example(command: &CargoCommand) -> anyhow::Result<()> { + match *command { + 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(TestRunError::PathConversionError)?; + + // command is either build or run + let cargo_run_result = run_command(command)?; + println!("{}", cargo_run_result.output); + + if let CargoCommand::Run { .. } = &command { + 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); + + Ok(()) + } + CargoCommand::Size { .. } => { + let cargo_run_result = run_command(command)?; + println!("{}", cargo_run_result.output); + Ok(()) + } + } +} |
