diff options
Diffstat (limited to 'xtask/src')
-rw-r--r-- | xtask/src/build.rs | 53 | ||||
-rw-r--r-- | xtask/src/command.rs | 162 | ||||
-rw-r--r-- | xtask/src/main.rs | 388 |
3 files changed, 603 insertions, 0 deletions
diff --git a/xtask/src/build.rs b/xtask/src/build.rs new file mode 100644 index 00000000..a8c19aac --- /dev/null +++ b/xtask/src/build.rs @@ -0,0 +1,53 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use crate::{command::BuildMode, TestRunError}; + +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)) +} + +pub fn build_hexpath( + example: &str, + features: Option<&str>, + build_mode: BuildMode, + build_num: u32, +) -> anyhow::Result<String> { + let features = match features { + Some(f) => f, + None => "", + }; + + let filename = format!("{}_{}_{}_{}.hex", example, features, build_mode, build_num); + + let mut path = PathBuf::from(HEX_BUILD_ROOT); + path.push(filename); + + path.into_os_string() + .into_string() + .map_err(|e| anyhow::Error::new(TestRunError::PathConversionError(e))) +} + +pub fn compare_builds(file_1: String, file_2: String) -> anyhow::Result<()> { + let buf_1 = std::fs::read_to_string(file_1.clone())?; + let buf_2 = std::fs::read_to_string(file_2.clone())?; + + if buf_1 != buf_2 { + return Err(anyhow::Error::new(TestRunError::FileCmpError { + file_1, + file_2, + })); + } + + Ok(()) +} diff --git a/xtask/src/command.rs b/xtask/src/command.rs new file mode 100644 index 00000000..8bf49849 --- /dev/null +++ b/xtask/src/command.rs @@ -0,0 +1,162 @@ +use crate::RunResult; +use core::fmt; +use os_pipe::pipe; +use std::{fs::File, io::Read, path::Path, process::Command}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BuildMode { + Release, + Debug, +} + +pub enum CargoCommand<'a> { + Run { + example: &'a str, + target: &'a str, + features: Option<&'a str>, + mode: BuildMode, + }, + Build { + example: &'a str, + target: &'a str, + features: Option<&'a str>, + mode: BuildMode, + }, + Objcopy { + example: &'a str, + target: &'a str, + features: Option<&'a str>, + ihex: &'a str, + }, + Size { + example_paths: Vec<&'a Path>, + }, + Clean, +} + +impl<'a> CargoCommand<'a> { + fn name(&self) -> &str { + match self { + CargoCommand::Run { .. } => "run", + CargoCommand::Size { example_paths: _ } => "rust-size", + CargoCommand::Clean => "clean", + CargoCommand::Build { .. } => "build", + CargoCommand::Objcopy { .. } => "objcopy", + } + } + + pub fn args(&self) -> Vec<&str> { + match self { + CargoCommand::Run { + example, + target, + features, + mode, + } + | CargoCommand::Build { + example, + target, + features, + mode, + } => { + let mut args = vec![self.name(), "--example", example, "--target", target]; + + 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_paths } => { + example_paths.iter().map(|p| p.to_str().unwrap()).collect() + } + CargoCommand::Clean => vec!["clean"], + CargoCommand::Objcopy { + example, + target, + features, + ihex, + } => { + let mut args = vec![self.name(), "--example", example, "--target", target]; + + if let Some(feature_name) = features { + args.extend_from_slice(&["--features", feature_name]); + } + + // this always needs to go at the end + args.extend_from_slice(&["--", "-O", "ihex", ihex]); + args + } + } + } + + pub fn command(&self) -> &str { + match self { + // we need to cheat a little here: + // `cargo size` can't be ran on multiple files, so we're using `rust-size` instead – + // which isn't a command that starts wizh `cargo`. So we're sneakily swapping them out :) + CargoCommand::Size { .. } => "rust-size", + _ => "cargo", + } + } +} + +impl BuildMode { + 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 sucessful. +/// returns Ok in case the run went as expected, +/// Err otherwise +pub fn run_successful(run: &RunResult, expected_output_file: String) -> anyhow::Result<()> { + let mut file_handle = File::open(expected_output_file)?; + let mut expected_output = String::new(); + file_handle.read_to_string(&mut expected_output)?; + if expected_output == run.output && run.exit_status.success() { + Ok(()) + } else { + Err(anyhow::anyhow!( + "Run failed with exit status {}: {}", + run.exit_status, + run.output + )) + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..3243b98e --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,388 @@ +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::{build_hexpath, compare_builds, 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 { + #[structopt(short, long)] + target: String, +} + +#[derive(Debug)] +pub struct RunResult { + exit_status: ExitStatus, + output: String, +} + +#[derive(Debug)] +enum TestRunError { + FileCmpError { file_1: String, file_2: 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 { file_1, file_2 } => { + write!(f, "Differing output in Files: {} {}", file_1, file_2) + } + 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 == false { + bail!("xtasks can only be executed from the root of the `cortex-m-rtic` repository"); + } + + let targets = [ARMV7M, ARMV6M]; + let examples = &[ + "idle", + "init", + "hardware", + "preempt", + "binds", + "resource", + "lock", + "multilock", + "only-shared-access", + "task", + "message", + "capacity", + "not-sync", + "generics", + "pool", + "ramfunc", + "peripherals-taken", + ]; + + let opts = Options::from_args(); + let target = &opts.target; + + init_build_dir()?; + + if target == "all" { + for t in targets { + run_test(t, examples)?; + build_test(t, examples)?; + } + } else if targets.contains(&target.as_str()) { + run_test(&target, examples)?; + build_test(&target, examples)?; + } else { + eprintln!( + "The target you specified is not available. Available targets are:\ + \n{:?}\n\ + as well as `all` (testing on all of the above)", + targets + ); + process::exit(1); + } + + Ok(()) +} + +fn run_test(target: &str, examples: &[&str]) -> anyhow::Result<()> { + for example in examples { + match *example { + "pool" => { + if target != ARMV6M { + // check this one manually because addresses printed in `pool.run` may vary + let features_v7 = Some("__v7"); + + let debug_run_result = run_command(&CargoCommand::Run { + example, + target, + features: features_v7, + mode: BuildMode::Debug, + })?; + + if debug_run_result.exit_status.success() { + print_from_output("foo(0x2", &debug_run_result.output); + print_from_output("bar(0x2", &debug_run_result.output); + } + + let hexpath = &build_hexpath(*example, features_v7, BuildMode::Debug, 1)?; + + run_command(&CargoCommand::Objcopy { + example, + target, + features: features_v7, + ihex: hexpath, + })?; + + let release_run_result = run_command(&CargoCommand::Run { + example, + target, + features: features_v7, + mode: BuildMode::Release, + })?; + + if release_run_result.exit_status.success() { + print_from_output("foo(0x2", &release_run_result.output); + print_from_output("bar(0x2", &release_run_result.output); + } + + let hexpath = &build_hexpath(*example, features_v7, BuildMode::Release, 1)?; + run_command(&CargoCommand::Objcopy { + example, + target, + features: features_v7, + ihex: hexpath, + })?; + } + } + "types" => { + let features_v7 = Some("__v7"); + + // TODO this example doesn't exist anymore, can we remove this case? + if target != ARMV6M { + arm_example( + &CargoCommand::Run { + example, + target, + features: features_v7, + mode: BuildMode::Debug, + }, + 1, + )?; + arm_example( + &CargoCommand::Run { + example, + target, + features: features_v7, + mode: BuildMode::Release, + }, + 1, + )?; + } + } + _ => { + arm_example( + &CargoCommand::Run { + example, + target, + features: None, + mode: BuildMode::Debug, + }, + 1, + )?; + + if *example == "types" { + arm_example( + &CargoCommand::Run { + example, + target, + features: None, + mode: BuildMode::Release, + }, + 1, + )?; + } else { + arm_example( + &CargoCommand::Build { + example, + target, + features: None, + mode: BuildMode::Release, + }, + 1, + )?; + } + } + } + } + + Ok(()) +} + +// run example binary `example` +fn arm_example(command: &CargoCommand, build_num: u32) -> anyhow::Result<()> { + match *command { + CargoCommand::Run { + example, + target, + features, + mode, + } + | CargoCommand::Build { + example, + target, + features, + mode, + } => { + let run_file = format!("{}.run", example); + let expected_output_file = ["ci", "expected", &run_file] + .iter() + .collect::<PathBuf>() + .into_os_string() + .into_string() + .map_err(|e| TestRunError::PathConversionError(e))?; + + // command is either build or run + let cargo_run_result = run_command(&command)?; + println!("{}", cargo_run_result.output); + + match &command { + CargoCommand::Run { .. } => { + if run_successful(&cargo_run_result, expected_output_file).is_err() { + return Err(anyhow::Error::new(TestRunError::CommandError( + cargo_run_result, + ))); + } + } + _ => (), + } + + // now, prepare to objcopy + let hexpath = build_hexpath(example, features, mode, build_num)?; + + run_command(&CargoCommand::Objcopy { + example, + target, + features, + ihex: &hexpath, + })?; + + Ok(()) + } + _ => Err(anyhow::Error::new(TestRunError::IncompatibleCommand)), + } +} + +fn build_test(target: &str, examples: &[&str]) -> anyhow::Result<()> { + run_command(&CargoCommand::Clean)?; + + let mut built = vec![]; + let build_path: PathBuf = ["target", target, "debug", "examples"].iter().collect(); + + for example in examples { + match *example { + "pool" | "types" => { + if target != ARMV6M { + let features_v7 = Some("__v7"); + + arm_example( + &CargoCommand::Build { + target, + example, + mode: BuildMode::Debug, + features: features_v7, + }, + 2, + )?; + let file_1 = build_hexpath(example, features_v7, BuildMode::Debug, 1)?; + let file_2 = build_hexpath(example, features_v7, BuildMode::Debug, 2)?; + + compare_builds(file_1, file_2)?; + + arm_example( + &CargoCommand::Build { + target, + example, + mode: BuildMode::Release, + features: features_v7, + }, + 2, + )?; + let file_1 = build_hexpath(example, features_v7, BuildMode::Release, 1)?; + let file_2 = build_hexpath(example, features_v7, BuildMode::Release, 2)?; + + compare_builds(file_1, file_2)?; + + built.push(build_path.join(example)); + } + } + _ => { + let no_features = None; + arm_example( + &CargoCommand::Build { + target, + example, + mode: BuildMode::Debug, + features: no_features, + }, + 2, + )?; + let file_1 = build_hexpath(example, no_features, BuildMode::Debug, 1)?; + let file_2 = build_hexpath(example, no_features, BuildMode::Debug, 2)?; + + compare_builds(file_1, file_2)?; + + arm_example( + &CargoCommand::Build { + target, + example, + mode: BuildMode::Release, + features: no_features, + }, + 2, + )?; + let file_1 = build_hexpath(example, no_features, BuildMode::Release, 1)?; + let file_2 = build_hexpath(example, no_features, BuildMode::Release, 2)?; + + compare_builds(file_1, file_2)?; + + built.push(build_path.join(example)); + } + } + } + + let example_paths: Vec<&Path> = built.iter().map(|p| p.as_path()).collect(); + let size_run_result = run_command(&CargoCommand::Size { example_paths })?; + + if size_run_result.exit_status.success() { + println!("{}", size_run_result.output); + } + + Ok(()) +} + +/// Check if lines in `output` contain `pattern` and print matching lines +fn print_from_output(pattern: &str, lines: &str) { + let lines = lines.split("\n"); + for line in lines { + if line.contains(pattern) { + println!("{}", line); + } + } +} |