chore(ci): CI grouping and refactoring (#1024)

This commit is contained in:
David Chavez 2023-12-04 17:14:50 +01:00 committed by GitHub
parent 8fc52113bc
commit 82f5722ca1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 242 additions and 58 deletions

View File

@ -116,7 +116,7 @@ jobs:
tar xj -C $HOME/.cargo/bin
- name: run checks & tests
run: ${{ matrix.coverage-flags }} ${{ matrix.wgpu-flags }} CI_RUN=1 cargo xtask run-checks ${{ matrix.test }}
run: ${{ matrix.coverage-flags }} ${{ matrix.wgpu-flags }} cargo xtask run-checks ${{ matrix.test }}
- name: Codecov upload
if: matrix.rust == 'stable' && matrix.test == 'std' && runner.os == 'Linux'
@ -145,4 +145,4 @@ jobs:
tar xz -C $HOME/.cargo/bin
- name: run spelling checks using typos
run: CI_RUN=1 cargo xtask run-checks typos
run: cargo xtask run-checks typos

View File

@ -9,3 +9,6 @@ license = "MIT OR Apache-2.0"
[dependencies]
anyhow = "1.0.75"
clap = { version = "4.4.8", features = ["derive"] }
env_logger = "0.10.0"
log = "0.4.17"
serde_json = { version = "1" }

67
xtask/src/logging.rs Normal file
View File

@ -0,0 +1,67 @@
use std::io::Write;
/// Initialise and create a `env_logger::Builder` which follows the
/// GitHub Actions logging syntax when running on CI.
pub fn init_logger() -> env_logger::Builder {
let mut builder = env_logger::Builder::from_default_env();
builder.target(env_logger::Target::Stdout);
// Find and setup the correct log level
builder.filter(None, get_log_level());
builder.write_style(env_logger::WriteStyle::Always);
// Custom Formatter for Github Actions
if std::env::var("CI").is_ok() {
builder.format(|buf, record| match record.level().as_str() {
"DEBUG" => writeln!(buf, "::debug:: {}", record.args()),
"WARN" => writeln!(buf, "::warning:: {}", record.args()),
"ERROR" => {
writeln!(buf, "::error:: {}", record.args())
}
_ => writeln!(buf, "{}", record.args()),
});
}
builder
}
/// Determine the LogLevel for the logger
fn get_log_level() -> log::LevelFilter {
// DEBUG
match std::env::var("DEBUG") {
Ok(_value) => return log::LevelFilter::Debug,
Err(_err) => (),
}
// ACTIONS_RUNNER_DEBUG
match std::env::var("ACTIONS_RUNNER_DEBUG") {
Ok(_value) => return log::LevelFilter::Debug,
Err(_err) => (),
};
log::LevelFilter::Info
}
/// Group Macro
#[macro_export]
macro_rules! group {
// group!()
($($arg:tt)*) => {
let title = format!($($arg)*);
if std::env::var("CI").is_ok() {
log!(log::Level::Info, "::group::{}", title)
} else {
log!(log::Level::Info, "{}", title)
}
};
}
/// End Group Macro
#[macro_export]
macro_rules! endgroup {
// endgroup!()
() => {
if std::env::var("CI").is_ok() {
log!(log::Level::Info, "::endgroup::")
}
};
}

View File

@ -1,7 +1,12 @@
use clap::{Parser, Subcommand};
mod logging;
mod publish;
mod runchecks;
mod utils;
#[macro_use]
extern crate log;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]

View File

@ -4,7 +4,11 @@
//!
//! It is also used to check that the code is formatted correctly and passes clippy.
use crate::logging::init_logger;
use crate::utils::{format_duration, get_workspaces, WorkspaceMemberType};
use crate::{endgroup, group};
use std::env;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::str;
use std::time::Instant;
@ -29,7 +33,7 @@ fn handle_child_process(mut child: Child, error: &str) {
// Run a command
fn run_command(command: &str, args: &[&str], command_error: &str, child_error: &str) {
// Format command
println!("{command} {}\n\n", args.join(" "));
info!("{command} {}\n\n", args.join(" "));
// Run command as child process
let command = Command::new(command)
@ -45,31 +49,48 @@ fn run_command(command: &str, args: &[&str], command_error: &str, child_error: &
// Define and run rustup command
fn rustup(command: &str, target: &str) {
group!("Rustup: {} add {}", command, target);
run_command(
"rustup",
&[command, "add", target],
"Failed to run rustup",
"Failed to wait for rustup child process",
)
);
endgroup!();
}
// Define and run a cargo command
fn run_cargo(command: &str, params: Params, error: &str) {
run_cargo_with_path::<String>(command, params, None, error)
}
// Define and run a cargo command with curr dir
fn run_cargo_with_path<P: AsRef<Path>>(
command: &str,
params: Params,
path: Option<P>,
error: &str,
) {
// Print cargo command
println!("\ncargo {} {}\n", command, params);
info!("cargo {} {}\n", command, params);
// Run cargo
let cargo = Command::new("cargo")
let mut cargo = Command::new("cargo");
cargo
.env("CARGO_INCREMENTAL", "0")
.arg(command)
.args(params.params)
.stdout(Stdio::inherit()) // Send stdout directly to terminal
.stderr(Stdio::inherit()) // Send stderr directly to terminal
.spawn()
.expect(error);
.stderr(Stdio::inherit()); // Send stderr directly to terminal
if let Some(path) = path {
cargo.current_dir(path);
}
let cargo_process = cargo.spawn().expect(error);
// Handle cargo child process
handle_child_process(cargo, "Failed to wait for cargo child process");
handle_child_process(cargo_process, "Failed to wait for cargo child process");
}
// Run cargo build command
@ -104,17 +125,18 @@ fn cargo_test(params: Params) {
// Run cargo fmt command
fn cargo_fmt() {
// Run cargo fmt
group!("Cargo: fmt");
run_cargo(
"fmt",
["--check", "--all", "--", "--color=always"].into(),
"Failed to run cargo fmt",
);
endgroup!();
}
// Run cargo clippy command
fn cargo_clippy() {
if std::env::var("CI_RUN").is_ok() {
if std::env::var("CI").is_ok() {
return;
}
// Run cargo clippy
@ -133,7 +155,7 @@ fn cargo_doc(params: Params) {
// Build and test a crate in a no_std environment
fn build_and_test_no_std<const N: usize>(crate_name: &str, extra_args: [&str; N]) {
println!("\nRun checks for `{}` crate", crate_name);
group!("Checks: {} (no-std)", crate_name);
// Run cargo build --no-default-features
cargo_build(Params::from(["-p", crate_name, "--no-default-features"]) + extra_args);
@ -162,6 +184,8 @@ fn build_and_test_no_std<const N: usize>(crate_name: &str, extra_args: [&str; N]
ARM_TARGET,
]) + extra_args,
);
endgroup!();
}
// Setup code coverage
@ -201,8 +225,6 @@ fn run_grcov() {
// Run no_std checks
fn no_std_checks() {
println!("Checks for no_std environment...\n\n");
// Install wasm32 target
rustup("target", WASM32_TARGET);
@ -224,20 +246,22 @@ fn no_std_checks() {
// Test burn-core with tch and wgpu backend
fn burn_core_std() {
println!("\n\nRun checks for burn-core crate with tch and wgpu backend");
// Run cargo test --features test-tch
group!("Test: burn-core (tch)");
cargo_test(["-p", "burn-core", "--features", "test-tch"].into());
endgroup!();
// Run cargo test --features test-wgpu
if std::env::var("DISABLE_WGPU").is_err() {
group!("Test: burn-core (wgpu)");
cargo_test(["-p", "burn-core", "--features", "test-wgpu"].into());
endgroup!();
}
}
// Test burn-dataset features
fn burn_dataset_features_std() {
println!("\n\nRun checks for burn-dataset features");
group!("Checks: burn-dataset (all-features)");
// Run cargo build --all-features
cargo_build(["-p", "burn-dataset", "--all-features"].into());
@ -247,13 +271,17 @@ fn burn_dataset_features_std() {
// Run cargo doc --all-features
cargo_doc(["-p", "burn-dataset", "--all-features"].into());
endgroup!();
}
// Test burn-candle with accelerate (macOS only)
// Leverages the macOS Accelerate framework: https://developer.apple.com/documentation/accelerate
#[cfg(target_os = "macos")]
fn burn_candle_accelerate() {
group!("Checks: burn-candle (accelerate)");
cargo_test(["-p", "burn-candle", "--features", "accelerate"].into());
endgroup!();
}
fn std_checks() {
@ -265,31 +293,38 @@ fn std_checks() {
let is_coverage = std::env::var("COVERAGE").is_ok();
let disable_wgpu = std::env::var("DISABLE_WGPU").is_ok();
println!("Running std checks");
// Check format
cargo_fmt();
// Check clippy lints
cargo_clippy();
// Build each workspace
if disable_wgpu {
cargo_build(["--workspace", "--exclude=xtask", "--exclude=burn-wgpu"].into());
} else {
cargo_build(["--workspace", "--exclude=xtask"].into());
}
// Produce documentation for each workspace
group!("Docs: workspaces");
cargo_doc(["--workspace"].into());
endgroup!();
// Setup code coverage
if is_coverage {
setup_coverage();
}
// Test each workspace
cargo_test(["--workspace"].into());
// Build & test each workspace
let workspaces = get_workspaces(WorkspaceMemberType::Crate);
for workspace in workspaces {
if disable_wgpu && workspace.name == "burn-wgpu" {
continue;
}
if workspace.name == "burn-tch" {
continue;
}
group!("Checks: {}", workspace.name);
cargo_build(Params::from(["-p", &workspace.name]));
cargo_test(Params::from(["-p", &workspace.name]));
endgroup!();
}
// Test burn-candle with accelerate (macOS only)
#[cfg(target_os = "macos")]
@ -316,12 +351,12 @@ fn check_typos() {
// Do not run cargo install on CI to speed up the computation.
// Check whether the file has been installed on
if std::env::var("CI_RUN").is_err() && !typos_cli_path.exists() {
if std::env::var("CI").is_err() && !typos_cli_path.exists() {
// Install typos-cli
cargo_install(["typos-cli", "--version", "1.16.5"].into());
}
println!("Running typos check \n\n");
info!("Running typos check \n\n");
// Run typos command as child process
let typos = Command::new("typos")
@ -335,34 +370,21 @@ fn check_typos() {
}
fn check_examples() {
println!("Checking examples compile \n\n");
std::fs::read_dir("examples").unwrap().for_each(|dir| {
let dir = dir.unwrap();
let path = dir.path();
// Skip if not a directory
if !path.is_dir() {
return;
let workspaces = get_workspaces(WorkspaceMemberType::Example);
for workspace in workspaces {
if workspace.name == "notebook" {
continue;
}
if path.file_name().unwrap().to_str().unwrap() == "notebook" {
// not a crate
return;
}
let path = path.to_str().unwrap();
println!("Checking {path} \n\n");
let child = Command::new("cargo")
.arg("check")
.arg("--examples")
.current_dir(dir.path())
.stdout(Stdio::inherit()) // Send stdout directly to terminal
.stderr(Stdio::inherit()) // Send stderr directly to terminal
.spawn()
.expect("Failed to check examples");
// Handle typos child process
handle_child_process(child, "Failed to wait for examples child process");
});
group!("Checks: Example - {}", workspace.name);
run_cargo_with_path(
"check",
["--examples"].into(),
Some(workspace.path),
"Failed to check example",
);
endgroup!();
}
}
#[derive(clap::ValueEnum, Default, Copy, Clone, PartialEq, Eq)]
@ -381,6 +403,9 @@ pub enum CheckType {
}
pub fn run(env: CheckType) -> anyhow::Result<()> {
// Setup logger
init_logger().init();
// Start time measurement
let start = Instant::now();
@ -411,7 +436,10 @@ pub fn run(env: CheckType) -> anyhow::Result<()> {
let duration = start.elapsed();
// Print duration
println!("Time elapsed for the current execution: {:?}", duration);
info!(
"\x1B[32;1mTime elapsed for the current execution: {}\x1B[0m",
format_duration(&duration)
);
Ok(())
}

81
xtask/src/utils.rs Normal file
View File

@ -0,0 +1,81 @@
use serde_json::Value;
use std::{process::Command, time::Duration};
pub(crate) enum WorkspaceMemberType {
Crate,
Example,
}
#[derive(Debug)]
pub(crate) struct WorkspaceMember {
pub(crate) name: String,
pub(crate) path: String,
}
impl WorkspaceMember {
fn new(name: String, path: String) -> Self {
Self { name, path }
}
}
/// Get project workspaces
pub(crate) fn get_workspaces(w_type: WorkspaceMemberType) -> Vec<WorkspaceMember> {
// Run `cargo metadata` command to get project metadata
let output = Command::new("cargo")
.arg("metadata")
.output()
.expect("Failed to execute command");
// Parse the JSON output
let metadata: Value = serde_json::from_slice(&output.stdout).expect("Failed to parse JSON");
// Extract workspaces from the metadata, excluding examples/ and xtask
let workspaces = metadata["workspace_members"]
.as_array()
.expect("Expected an array of workspace members")
.iter()
.filter_map(|member| {
let parts: Vec<_> = member.as_str()?.split_whitespace().collect();
let (workspace_name, workspace_path) =
(parts.first()?.to_owned(), parts.last()?.to_owned());
let workspace_path = workspace_path.replace("(path+file://", "").replace(')', "");
match w_type {
WorkspaceMemberType::Crate
if workspace_name != "xtask" && !workspace_path.contains("examples/") =>
{
Some(WorkspaceMember::new(
workspace_name.to_string(),
workspace_path.to_string(),
))
}
WorkspaceMemberType::Example
if workspace_name != "xtask" && workspace_path.contains("examples/") =>
{
Some(WorkspaceMember::new(
workspace_name.to_string(),
workspace_path.to_string(),
))
}
_ => None,
}
})
.collect();
workspaces
}
/// Print duration as HH:MM:SS format
pub(crate) fn format_duration(duration: &Duration) -> String {
let seconds = duration.as_secs();
let minutes = seconds / 60;
let hours = minutes / 60;
let remaining_minutes = minutes % 60;
let remaining_seconds = seconds % 60;
format!(
"{:02}:{:02}:{:02}",
hours, remaining_minutes, remaining_seconds
)
}