[xtask] New commands: dependencies and vulnerabilities (#1181)

* [xtask] Add support for vulnerabilities check in xtask

cargo +nightly xtask vulnerability --help

* [xtask] Add support for dependencies check in xtask

cargo xtask dependencies --help

* Make sure all vulnerabilities checks are called with cargo +nightly

* Fix clippy errors

* Use automatic links in docstrings

* Move run function to the top of the file

* Skip dependencies documenation

* pub -> pub(crate)

* Change return type of Sanitizer::flags to &str

* Move the run functions as an impl function of check types enums

* Remove Type suffix in check type enums

* Fix wrong variable name

* cargo_commande -> cargo_crate

* Improve robustness of is_target_supported and add tests

* Reorganize utils module into a directory
This commit is contained in:
Sylvain Benner 2024-01-27 08:40:41 -05:00 committed by GitHub
parent b9bd42959b
commit 3814c4c9fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 839 additions and 196 deletions

View File

@ -12,3 +12,6 @@ clap = { version = "4.4.8", features = ["derive"] }
env_logger = "0.10.0"
log = "0.4.17"
serde_json = { version = "1" }
[dev-dependencies]
rstest.workspace = true

106
xtask/src/dependencies.rs Normal file
View File

@ -0,0 +1,106 @@
use std::{collections::HashMap, time::Instant};
use crate::{
endgroup, group,
logging::init_logger,
utils::{
cargo::{ensure_cargo_crate_is_installed, run_cargo},
rustup::is_current_toolchain_nightly,
time::format_duration,
Params,
},
};
#[derive(clap::ValueEnum, Default, Copy, Clone, PartialEq, Eq)]
pub(crate) enum DependencyCheck {
/// Run all dependency checks.
#[default]
All,
/// Perform an audit of all dependencies using the cargo-audit crate `<https://crates.io/crates/cargo-audit>`
Audit,
/// Run cargo-deny check `<https://crates.io/crates/cargo-deny>`
Deny,
/// Run cargo-udeps to find unused dependencies `<https://crates.io/crates/cargo-udeps>`
Unused,
}
impl DependencyCheck {
pub(crate) fn run(&self) -> anyhow::Result<()> {
// Setup logger
init_logger().init();
// Start time measurement
let start = Instant::now();
match self {
Self::Audit => cargo_audit(),
Self::Deny => cargo_deny(),
Self::Unused => cargo_udeps(),
Self::All => {
cargo_audit();
cargo_deny();
cargo_udeps();
}
}
// Stop time measurement
//
// Compute runtime duration
let duration = start.elapsed();
// Print duration
info!(
"\x1B[32;1mTime elapsed for the current execution: {}\x1B[0m",
format_duration(&duration)
);
Ok(())
}
}
/// Run cargo-audit
fn cargo_audit() {
ensure_cargo_crate_is_installed("cargo-audit");
// Run cargo audit
group!("Cargo: run audit checks");
run_cargo(
"audit",
Params::from([]),
HashMap::new(),
"Cargo audit should be installed and it should correctly run",
);
endgroup!();
}
/// Run cargo-deny
fn cargo_deny() {
ensure_cargo_crate_is_installed("cargo-deny");
// Run cargo deny
group!("Cargo: run deny checks");
run_cargo(
"deny",
Params::from(["check"]),
HashMap::new(),
"Cargo deny should be installed and it should correctly run",
);
endgroup!();
}
/// Run cargo-udeps
fn cargo_udeps() {
if is_current_toolchain_nightly() {
ensure_cargo_crate_is_installed("cargo-udeps");
// Run cargo udeps
group!("Cargo: run unused dependencies checks");
run_cargo(
"udeps",
Params::from([]),
HashMap::new(),
"Cargo udeps should be installed and it should correctly run",
);
endgroup!();
} else {
error!(
"You must use 'cargo +nightly' to check for unused dependencies.
Install a nightly toolchain with 'rustup toolchain install nightly'."
)
}
}

View File

@ -1,9 +1,11 @@
use clap::{Parser, Subcommand};
mod dependencies;
mod logging;
mod publish;
mod runchecks;
mod utils;
mod vulnerabilities;
#[macro_use]
extern crate log;
@ -17,6 +19,11 @@ struct Args {
#[derive(Subcommand)]
enum Command {
/// Run the specified dependencies check locally
Dependencies {
/// The dependency check to run
dependency_check: dependencies::DependencyCheck,
},
/// Publish a crate to crates.io
Publish {
/// The name of the crate to publish on crates.io
@ -27,13 +34,23 @@ enum Command {
/// The environment to run checks against
env: runchecks::CheckType,
},
/// Run the specified vulnerability check locally. These commands must be called with 'cargo +nightly'.
Vulnerabilities {
/// The vulnerability check to run.
/// For the reference visit the page `<https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html>`
vulnerability_check: vulnerabilities::VulnerabilityCheck,
},
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
match args.command {
Command::RunChecks { env } => runchecks::run(env),
Command::Dependencies { dependency_check } => dependency_check.run(),
Command::Publish { name } => publish::run(name),
Command::RunChecks { env } => env.run(),
Command::Vulnerabilities {
vulnerability_check,
} => vulnerability_check.run(),
}
}

View File

@ -5,11 +5,16 @@
//! 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::utils::cargo::{run_cargo, run_cargo_with_path};
use crate::utils::process::{handle_child_process, run_command};
use crate::utils::rustup::{rustup_add_component, rustup_add_target};
use crate::utils::time::format_duration;
use crate::utils::workspace::{get_workspaces, WorkspaceMemberType};
use crate::utils::Params;
use crate::{endgroup, group};
use std::collections::HashMap;
use std::env;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::process::{Command, Stdio};
use std::str;
use std::time::Instant;
@ -17,124 +22,110 @@ use std::time::Instant;
const WASM32_TARGET: &str = "wasm32-unknown-unknown";
const ARM_TARGET: &str = "thumbv7m-none-eabi";
// Handle child process
fn handle_child_process(mut child: Child, error: &str) {
// Wait for the child process to finish
let status = child.wait().expect(error);
#[derive(clap::ValueEnum, Default, Copy, Clone, PartialEq, Eq)]
pub(crate) enum CheckType {
/// Run all checks.
#[default]
All,
/// Run `std` environment checks
Std,
/// Run `no-std` environment checks
NoStd,
/// Check for typos
Typos,
/// Test the examples
Examples,
}
// If exit status is not a success, terminate the process with an error
if !status.success() {
// Use the exit code associated to a command to terminate the process,
// if any exit code had been found, use the default value 1
std::process::exit(status.code().unwrap_or(1));
impl CheckType {
pub(crate) fn run(&self) -> anyhow::Result<()> {
// Setup logger
init_logger().init();
// Start time measurement
let start = Instant::now();
// The environment can assume ONLY "std", "no_std", "typos", "examples"
//
// Depending on the input argument, the respective environment checks
// are run.
//
// If no environment has been passed, run all checks.
match self {
Self::Std => std_checks(),
Self::NoStd => no_std_checks(),
Self::Typos => check_typos(),
Self::Examples => check_examples(),
Self::All => {
/* Run all checks */
check_typos();
std_checks();
no_std_checks();
check_examples();
}
}
// Run a command
fn run_command(command: &str, args: &[&str], command_error: &str, child_error: &str) {
// Format command
info!("{command} {}\n\n", args.join(" "));
// Stop time measurement
//
// Compute runtime duration
let duration = start.elapsed();
// Run command as child process
let command = Command::new(command)
.args(args)
.stdout(Stdio::inherit()) // Send stdout directly to terminal
.stderr(Stdio::inherit()) // Send stderr directly to terminal
.spawn()
.expect(command_error);
// Handle command child process
handle_child_process(command, 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",
// Print duration
info!(
"\x1B[32;1mTime elapsed for the current execution: {}\x1B[0m",
format_duration(&duration)
);
endgroup!();
Ok(())
}
}
// 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
info!("cargo {} {}\n", command, params);
// Run 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
if let Some(path) = path {
cargo.current_dir(path);
}
let cargo_process = cargo.spawn().expect(error);
// Handle cargo child process
handle_child_process(cargo_process, "Failed to wait for cargo child process");
}
// Run cargo build command
/// Run cargo build command
fn cargo_build(params: Params) {
// Run cargo build
run_cargo(
"build",
params + "--color=always",
HashMap::new(),
"Failed to run cargo build",
);
}
// Run cargo install command
/// Run cargo install command
fn cargo_install(params: Params) {
// Run cargo install
run_cargo(
"install",
params + "--color=always",
HashMap::new(),
"Failed to run cargo install",
);
}
// Run cargo test command
/// Run cargo test command
fn cargo_test(params: Params) {
// Run cargo test
run_cargo(
"test",
params + "--color=always" + "--" + "--color=always",
HashMap::new(),
"Failed to run cargo test",
);
}
// Run cargo fmt command
/// Run cargo fmt command
fn cargo_fmt() {
group!("Cargo: fmt");
run_cargo(
"fmt",
["--check", "--all", "--", "--color=always"].into(),
HashMap::new(),
"Failed to run cargo fmt",
);
endgroup!();
}
// Run cargo clippy command
/// Run cargo clippy command
fn cargo_clippy() {
if std::env::var("CI").is_ok() {
return;
@ -143,14 +134,20 @@ fn cargo_clippy() {
run_cargo(
"clippy",
["--color=always", "--all-targets", "--", "-D", "warnings"].into(),
HashMap::new(),
"Failed to run cargo clippy",
);
}
// Run cargo doc command
/// Run cargo doc command
fn cargo_doc(params: Params) {
// Run cargo doc
run_cargo("doc", params + "--color=always", "Failed to run cargo doc");
run_cargo(
"doc",
params + "--color=always",
HashMap::new(),
"Failed to run cargo doc",
);
}
// Build and test a crate in a no_std environment
@ -191,7 +188,7 @@ fn build_and_test_no_std<const N: usize>(crate_name: &str, extra_args: [&str; N]
// Setup code coverage
fn setup_coverage() {
// Install llvm-tools-preview
rustup("component", "llvm-tools-preview");
rustup_add_component("llvm-tools-preview");
// Set coverage environment variables
env::set_var("RUSTFLAGS", "-Cinstrument-coverage");
@ -226,10 +223,10 @@ fn run_grcov() {
// Run no_std checks
fn no_std_checks() {
// Install wasm32 target
rustup("target", WASM32_TARGET);
rustup_add_target(WASM32_TARGET);
// Install ARM target
rustup("target", ARM_TARGET);
rustup_add_target(ARM_TARGET);
// Run checks for the following crates
build_and_test_no_std("burn", []);
@ -279,7 +276,7 @@ fn burn_dataset_features_std() {
cargo_test(["-p", "burn-dataset", "--all-features"].into());
// Run cargo doc --all-features
cargo_doc(["-p", "burn-dataset", "--all-features"].into());
cargo_doc(["-p", "burn-dataset", "--all-features", "--no-deps"].into());
endgroup!();
}
@ -315,7 +312,7 @@ fn std_checks() {
// Produce documentation for each workspace
group!("Docs: workspaces");
cargo_doc(["--workspace"].into());
cargo_doc(["--workspace", "--no-deps"].into());
endgroup!();
// Setup code coverage
@ -394,102 +391,10 @@ fn check_examples() {
run_cargo_with_path(
"check",
["--examples"].into(),
HashMap::new(),
Some(workspace.path),
"Failed to check example",
);
endgroup!();
}
}
#[derive(clap::ValueEnum, Default, Copy, Clone, PartialEq, Eq)]
pub enum CheckType {
/// Run all checks.
#[default]
All,
/// Run `std` environment checks
Std,
/// Run `no-std` environment checks
NoStd,
/// Check for typos
Typos,
/// Test the examples
Examples,
}
pub fn run(env: CheckType) -> anyhow::Result<()> {
// Setup logger
init_logger().init();
// Start time measurement
let start = Instant::now();
// The environment can assume ONLY "std", "no_std", "typos", "examples"
// as values.
//
// Depending on the input argument, the respective environment checks
// are run.
//
// If no environment has been passed, run all checks.
match env {
CheckType::Std => std_checks(),
CheckType::NoStd => no_std_checks(),
CheckType::Typos => check_typos(),
CheckType::Examples => check_examples(),
CheckType::All => {
/* Run all checks */
check_typos();
std_checks();
no_std_checks();
check_examples();
}
}
// Stop time measurement
//
// Compute runtime duration
let duration = start.elapsed();
// Print duration
info!(
"\x1B[32;1mTime elapsed for the current execution: {}\x1B[0m",
format_duration(&duration)
);
Ok(())
}
struct Params {
params: Vec<String>,
}
impl<const N: usize> From<[&str; N]> for Params {
fn from(value: [&str; N]) -> Self {
Self {
params: value.iter().map(|v| v.to_string()).collect(),
}
}
}
impl From<&str> for Params {
fn from(value: &str) -> Self {
Self {
params: vec![value.to_string()],
}
}
}
impl std::fmt::Display for Params {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.params.join(" ").as_str())
}
}
impl<Rhs: Into<Params>> std::ops::Add<Rhs> for Params {
type Output = Params;
fn add(mut self, rhs: Rhs) -> Self::Output {
let rhs: Params = rhs.into();
self.params.extend(rhs.params);
self
}
}

66
xtask/src/utils/cargo.rs Normal file
View File

@ -0,0 +1,66 @@
use std::{
collections::HashMap,
path::Path,
process::{Command, Stdio},
};
use crate::{endgroup, group, utils::process::handle_child_process};
use super::Params;
/// Run a cargo command
pub(crate) fn run_cargo(command: &str, params: Params, envs: HashMap<&str, String>, error: &str) {
run_cargo_with_path::<String>(command, params, envs, None, error)
}
/// Run acargo command with the passed directory as the current directory
pub(crate) fn run_cargo_with_path<P: AsRef<Path>>(
command: &str,
params: Params,
envs: HashMap<&str, String>,
path: Option<P>,
error: &str,
) {
info!("cargo {} {}\n", command, params.params.join(" "));
let mut cargo = Command::new("cargo");
cargo
.env("CARGO_INCREMENTAL", "0")
.envs(&envs)
.arg(command)
.args(&params.params)
.stdout(Stdio::inherit()) // Send stdout directly to terminal
.stderr(Stdio::inherit()); // Send stderr directly to terminal
if let Some(path) = path {
cargo.current_dir(path);
}
// Handle cargo child process
let cargo_process = cargo.spawn().expect(error);
handle_child_process(cargo_process, "Cargo process should run flawlessly");
}
/// Ensure that a cargo crate is installed
pub(crate) fn ensure_cargo_crate_is_installed(crate_name: &str) {
if !is_cargo_crate_installed(crate_name) {
group!("Cargo: install {} crate_name", crate_name);
run_cargo(
"install",
[crate_name].into(),
HashMap::new(),
&format!("{} should be installed", crate_name),
);
endgroup!();
}
}
/// Returns true if the passed cargo crate is installed locally
fn is_cargo_crate_installed(crate_name: &str) -> bool {
let output = Command::new("cargo")
.arg("install")
.arg("--list")
.output()
.expect("Should get the list of installed cargo commands");
let output_str = String::from_utf8_lossy(&output.stdout);
output_str.lines().any(|line| line.contains(crate_name))
}

49
xtask/src/utils/mod.rs Normal file
View File

@ -0,0 +1,49 @@
pub(crate) mod cargo;
pub(crate) mod process;
pub(crate) mod rustup;
pub(crate) mod time;
pub(crate) mod workspace;
pub(crate) struct Params {
params: Vec<String>,
}
impl<const N: usize> From<[&str; N]> for Params {
fn from(value: [&str; N]) -> Self {
Self {
params: value.iter().map(|v| v.to_string()).collect(),
}
}
}
impl From<&str> for Params {
fn from(value: &str) -> Self {
Self {
params: vec![value.to_string()],
}
}
}
impl From<Vec<&str>> for Params {
fn from(value: Vec<&str>) -> Self {
Self {
params: value.iter().map(|s| s.to_string()).collect(),
}
}
}
impl std::fmt::Display for Params {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.params.join(" ").as_str())
}
}
impl<Rhs: Into<Params>> std::ops::Add<Rhs> for Params {
type Output = Params;
fn add(mut self, rhs: Rhs) -> Self::Output {
let rhs: Params = rhs.into();
self.params.extend(rhs.params);
self
}
}

View File

@ -0,0 +1,31 @@
use std::process::{Child, Command, Stdio};
// Handle child process
pub(crate) fn handle_child_process(mut child: Child, error: &str) {
// Wait for the child process to finish
let status = child.wait().expect(error);
// If exit status is not a success, terminate the process with an error
if !status.success() {
// Use the exit code associated to a command to terminate the process,
// if any exit code had been found, use the default value 1
std::process::exit(status.code().unwrap_or(1));
}
}
// Run a command
pub(crate) fn run_command(command: &str, args: &[&str], command_error: &str, child_error: &str) {
// Format command
info!("{command} {}\n\n", args.join(" "));
// Run command as child process
let command = Command::new(command)
.args(args)
.stdout(Stdio::inherit()) // Send stdout directly to terminal
.stderr(Stdio::inherit()) // Send stderr directly to terminal
.spawn()
.expect(command_error);
// Handle command child process
handle_child_process(command, child_error);
}

68
xtask/src/utils/rustup.rs Normal file
View File

@ -0,0 +1,68 @@
use std::process::{Command, Stdio};
use crate::{endgroup, group, utils::process::handle_child_process};
use super::Params;
/// Run rustup command
pub(crate) fn rustup(command: &str, params: Params, expected: &str) {
info!("rustup {} {}\n", command, params);
// Run rustup
let mut rustup = Command::new("rustup");
rustup
.arg(command)
.args(params.params)
.stdout(Stdio::inherit()) // Send stdout directly to terminal
.stderr(Stdio::inherit()); // Send stderr directly to terminal
let cargo_process = rustup.spawn().expect(expected);
handle_child_process(cargo_process, "Failed to wait for rustup child process");
}
/// Add a Rust target
pub(crate) fn rustup_add_target(target: &str) {
group!("Rustup: add target {}", target);
rustup(
"target",
Params::from(["add", target]),
"Target should be added",
);
endgroup!();
}
/// Add a Rust component
pub(crate) fn rustup_add_component(component: &str) {
group!("Rustup: add component {}", component);
rustup(
"component",
Params::from(["add", component]),
"Component should be added",
);
endgroup!();
}
// Returns the output of the rustup command to get the installed targets
pub(crate) fn rustup_get_installed_targets() -> String {
let output = Command::new("rustup")
.args(["target", "list", "--installed"])
.stdout(Stdio::piped())
.output()
.expect("Rustup command should execute successfully");
String::from_utf8(output.stdout).expect("Output should be valid UTF-8")
}
/// Returns true if the current toolchain is the nightly
pub(crate) fn is_current_toolchain_nightly() -> bool {
let output = Command::new("rustup")
.arg("show")
.output()
.expect("Should get the list of installed Rust toolchains");
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
// look for the "rustc.*-nightly" line
if line.contains("rustc") && line.contains("-nightly") {
return true;
}
}
// assume we are using a stable toolchain if we did not find the nightly compiler
false
}

15
xtask/src/utils/time.rs Normal file
View File

@ -0,0 +1,15 @@
use std::time::Duration;
/// 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
)
}

View File

@ -1,5 +1,6 @@
use std::process::Command;
use serde_json::Value;
use std::{process::Command, time::Duration};
pub(crate) enum WorkspaceMemberType {
Crate,
@ -70,17 +71,3 @@ pub(crate) fn get_workspaces(w_type: WorkspaceMemberType) -> Vec<WorkspaceMember
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
)
}

View File

@ -0,0 +1,396 @@
use std::collections::HashMap;
use std::time::Instant;
use crate::logging::init_logger;
use crate::utils::cargo::{ensure_cargo_crate_is_installed, run_cargo};
use crate::utils::rustup::{
is_current_toolchain_nightly, rustup_add_component, rustup_get_installed_targets,
};
use crate::utils::time::format_duration;
use crate::utils::Params;
use crate::{endgroup, group};
use std::fmt;
#[derive(clap::ValueEnum, Default, Copy, Clone, PartialEq, Eq)]
pub(crate) enum VulnerabilityCheck {
/// Run all most useful vulnerability checks.
#[default]
All,
/// Run Address sanitizer (memory error detector)
AddressSanitizer,
/// Run LLVM Control Flow Integrity (CFI) (provides forward-edge control flow protection)
ControlFlowIntegrity,
/// Run newer variant of Address sanitizer (memory error detector similar to AddressSanitizer, but based on partial hardware assistance)
HWAddressSanitizer,
/// Run Kernel LLVM Control Flow Integrity (KCFI) (provides forward-edge control flow protection for operating systems kernels)
KernelControlFlowIntegrity,
/// Run Leak sanitizer (run-time memory leak detector)
LeakSanitizer,
/// Run memory sanitizer (detector of uninitialized reads)
MemorySanitizer,
/// Run another address sanitizer (like AddressSanitizer and HardwareAddressSanitizer but with lower overhead suitable for use as hardening for production binaries)
MemTagSanitizer,
/// Run nightly-only checks through cargo-careful `<https://crates.io/crates/cargo-careful>`
NightlyChecks,
/// Run SafeStack check (provides backward-edge control flow protection by separating
/// stack into safe and unsafe regions)
SafeStack,
/// Run ShadowCall check (provides backward-edge control flow protection - aarch64 only)
ShadowCallStack,
/// Run Thread sanitizer (data race detector)
ThreadSanitizer,
}
impl VulnerabilityCheck {
pub(crate) fn run(&self) -> anyhow::Result<()> {
// Setup logger
init_logger().init();
// Start time measurement
let start = Instant::now();
match self {
Self::NightlyChecks => cargo_careful(),
Self::AddressSanitizer => Sanitizer::Address.run_tests(),
Self::ControlFlowIntegrity => Sanitizer::CFI.run_tests(),
Self::HWAddressSanitizer => Sanitizer::HWAddress.run_tests(),
Self::KernelControlFlowIntegrity => Sanitizer::KCFI.run_tests(),
Self::LeakSanitizer => Sanitizer::Leak.run_tests(),
Self::MemorySanitizer => Sanitizer::Memory.run_tests(),
Self::MemTagSanitizer => Sanitizer::MemTag.run_tests(),
Self::SafeStack => Sanitizer::SafeStack.run_tests(),
Self::ShadowCallStack => Sanitizer::ShadowCallStack.run_tests(),
Self::ThreadSanitizer => Sanitizer::Thread.run_tests(),
Self::All => {
cargo_careful();
Sanitizer::Address.run_tests();
Sanitizer::Leak.run_tests();
Sanitizer::Memory.run_tests();
Sanitizer::SafeStack.run_tests();
Sanitizer::Thread.run_tests();
}
}
// Stop time measurement
//
// Compute runtime duration
let duration = start.elapsed();
// Print duration
info!(
"\x1B[32;1mTime elapsed for the current execution: {}\x1B[0m",
format_duration(&duration)
);
Ok(())
}
}
/// Run cargo-careful
fn cargo_careful() {
if is_current_toolchain_nightly() {
ensure_cargo_crate_is_installed("cargo-careful");
rustup_add_component("rust-src");
// prepare careful sysroot
group!("Cargo: careful setup");
run_cargo(
"careful",
Params::from(["setup"]),
HashMap::new(),
"Cargo sysroot should be available",
);
endgroup!();
// Run cargo careful
group!("Cargo: run careful checks");
run_cargo(
"careful",
Params::from(["test"]),
HashMap::new(),
"Cargo careful should be installed and it should correctly run",
);
endgroup!();
} else {
error!(
"You must use 'cargo +nightly' to run nightly checks.
Install a nightly toolchain with 'rustup toolchain install nightly'."
)
}
}
// Represents the various sanitizer available in nightly compiler
// source: https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html
#[allow(clippy::upper_case_acronyms)]
enum Sanitizer {
Address,
CFI,
HWAddress,
KCFI,
Leak,
Memory,
MemTag,
SafeStack,
ShadowCallStack,
Thread,
}
impl fmt::Display for Sanitizer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Sanitizer::Address => write!(f, "AddressSanitizer"),
Sanitizer::CFI => write!(f, "ControlFlowIntegrity"),
Sanitizer::HWAddress => write!(f, "HWAddressSanitizer"),
Sanitizer::KCFI => write!(f, "KernelControlFlowIntegrity"),
Sanitizer::Leak => write!(f, "LeakSanitizer"),
Sanitizer::Memory => write!(f, "MemorySanitizer"),
Sanitizer::MemTag => write!(f, "MemTagSanitizer"),
Sanitizer::SafeStack => write!(f, "SafeStack"),
Sanitizer::ShadowCallStack => write!(f, "ShadowCallStack"),
Sanitizer::Thread => write!(f, "ThreadSanitizer"),
}
}
}
impl Sanitizer {
const DEFAULT_RUSTFLAGS: &'static str = "-Copt-level=3";
fn run_tests(&self) {
if is_current_toolchain_nightly() {
group!("Sanitizer: {}", self.to_string());
let retriever = RustupTargetRetriever;
if self.is_target_supported(&retriever) {
let envs = vec![
(
"RUSTFLAGS",
format!("{} {}", self.flags(), Sanitizer::DEFAULT_RUSTFLAGS),
),
("RUSTDOCFLAGS", self.flags().to_string()),
];
let features = self.cargo_features();
let mut args = vec!["--", "--color=always", "--no-capture"];
args.extend(features);
run_cargo(
"test",
args.into(),
envs.into_iter().collect(),
"Failed to run cargo test",
);
} else {
info!("No supported target found for this sanitizer.");
}
endgroup!();
} else {
error!(
"You must use 'cargo +nightly' to run this check.
Install a nightly toolchain with 'rustup toolchain install nightly'."
)
}
}
fn flags(&self) -> &'static str {
match self {
Sanitizer::Address => "-Zsanitizer=address",
Sanitizer::CFI => "-Zsanitizer=cfi -Clto",
Sanitizer::HWAddress => "-Zsanitizer=hwaddress -Ctarget-feature=+tagged-globals",
Sanitizer::KCFI => "-Zsanitizer=kcfi",
Sanitizer::Leak => "-Zsanitizer=leak",
Sanitizer::Memory => "-Zsanitizer=memory -Zsanitizer-memory-track-origins",
Sanitizer::MemTag => "--Zsanitizer=memtag -Ctarget-feature=\"+mte\"",
Sanitizer::SafeStack => "-Zsanitizer=safestack",
Sanitizer::ShadowCallStack => "-Zsanitizer=shadow-call-stack",
Sanitizer::Thread => "-Zsanitizer=thread",
}
}
fn cargo_features(&self) -> Vec<&str> {
match self {
Sanitizer::CFI => vec!["-Zbuild-std", "--target x86_64-unknown-linux-gnu"],
_ => vec![],
}
}
fn supported_targets(&self) -> Vec<Target> {
match self {
Sanitizer::Address => vec![
Target::Aarch64AppleDarwin,
Target::Aarch64UnknownFuchsia,
Target::Aarch64UnknownLinuxGnu,
Target::X8664AppleDarwin,
Target::X8664UnknownFuchsia,
Target::X8664UnknownFreebsd,
Target::X8664UnknownLinuxGnu,
],
Sanitizer::CFI => vec![Target::X8664UnknownLinuxGnu],
Sanitizer::HWAddress => {
vec![Target::Aarch64LinuxAndroid, Target::Aarch64UnknownLinuxGnu]
}
Sanitizer::KCFI => vec![
Target::Aarch64LinuxAndroid,
Target::Aarch64UnknownLinuxGnu,
Target::X8664LinuxAndroid,
Target::X8664UnknownLinuxGnu,
],
Sanitizer::Leak => vec![
Target::Aarch64AppleDarwin,
Target::Aarch64UnknownLinuxGnu,
Target::X8664AppleDarwin,
Target::X8664UnknownLinuxGnu,
],
Sanitizer::Memory => vec![
Target::Aarch64UnknownLinuxGnu,
Target::X8664UnknownFreebsd,
Target::X8664UnknownLinuxGnu,
],
Sanitizer::MemTag => vec![Target::Aarch64LinuxAndroid, Target::Aarch64UnknownLinuxGnu],
Sanitizer::SafeStack => vec![Target::X8664UnknownLinuxGnu],
Sanitizer::ShadowCallStack => vec![Target::Aarch64LinuxAndroid],
Sanitizer::Thread => vec![
Target::Aarch64AppleDarwin,
Target::Aarch64UnknownLinuxGnu,
Target::X8664AppleDarwin,
Target::X8664UnknownFreebsd,
Target::X8664UnknownLinuxGnu,
],
}
}
// Returns true if the sanitizer is supported by the currently installed targets
fn is_target_supported<T: TargetRetriever>(&self, retriever: &T) -> bool {
let installed_targets = retriever.get_installed_targets();
let supported = self.supported_targets();
installed_targets.iter().any(|installed| {
let installed_target = Target::from_str(installed.trim()).unwrap_or(Target::Unknown);
supported.iter().any(|target| target == &installed_target)
})
}
}
// Constants for target names
const AARCH64_APPLE_DARWIN: &str = "aarch64-apple-darwin";
const AARCH64_LINUX_ANDROID: &str = "aarch64-linux-android";
const AARCH64_UNKNOWN_FUCHSIA: &str = "aarch64-unknown-fuchsia";
const AARCH64_UNKNOWN_LINUX_GNU: &str = "aarch64-unknown-linux-gnu";
const X8664_APPLE_DARWIN: &str = "x86_64-apple-darwin";
const X8664_LINUX_ANDROID: &str = "x86_64-linux-android";
const X8664_UNKNOWN_FUCHSIA: &str = "x86_64-unknown-fuchsia";
const X8664_UNKNOWN_FREEBSD: &str = "x86_64-unknown-freebsd";
const X8664_UNKNOWN_LINUX_GNU: &str = "x86_64-unknown-linux-gnu";
trait TargetRetriever {
fn get_installed_targets(&self) -> Vec<String>;
}
struct RustupTargetRetriever;
impl TargetRetriever for RustupTargetRetriever {
fn get_installed_targets(&self) -> Vec<String> {
rustup_get_installed_targets()
.lines()
.map(|s| s.to_string())
.collect()
}
}
// Represents Rust targets
// Remark: we list only the targets that are supported by sanitizers
#[derive(Debug, PartialEq)]
enum Target {
Aarch64AppleDarwin,
Aarch64LinuxAndroid,
Aarch64UnknownFuchsia,
Aarch64UnknownLinuxGnu,
X8664AppleDarwin,
X8664LinuxAndroid,
X8664UnknownFuchsia,
X8664UnknownFreebsd,
X8664UnknownLinuxGnu,
Unknown,
}
impl Target {
fn from_str(s: &str) -> Option<Self> {
match s {
AARCH64_APPLE_DARWIN => Some(Self::Aarch64AppleDarwin),
AARCH64_LINUX_ANDROID => Some(Self::Aarch64LinuxAndroid),
AARCH64_UNKNOWN_FUCHSIA => Some(Self::Aarch64UnknownFuchsia),
AARCH64_UNKNOWN_LINUX_GNU => Some(Self::Aarch64UnknownLinuxGnu),
X8664_APPLE_DARWIN => Some(Self::X8664AppleDarwin),
X8664_LINUX_ANDROID => Some(Self::X8664LinuxAndroid),
X8664_UNKNOWN_FUCHSIA => Some(Self::X8664UnknownFuchsia),
X8664_UNKNOWN_FREEBSD => Some(Self::X8664UnknownFreebsd),
X8664_UNKNOWN_LINUX_GNU => Some(Self::X8664UnknownLinuxGnu),
_ => None,
}
}
}
impl fmt::Display for Target {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let target_str = match self {
Target::Aarch64AppleDarwin => AARCH64_APPLE_DARWIN,
Target::Aarch64LinuxAndroid => AARCH64_LINUX_ANDROID,
Target::Aarch64UnknownFuchsia => AARCH64_UNKNOWN_FUCHSIA,
Target::Aarch64UnknownLinuxGnu => AARCH64_UNKNOWN_LINUX_GNU,
Target::X8664AppleDarwin => X8664_APPLE_DARWIN,
Target::X8664LinuxAndroid => X8664_LINUX_ANDROID,
Target::X8664UnknownFuchsia => X8664_UNKNOWN_FUCHSIA,
Target::X8664UnknownFreebsd => X8664_UNKNOWN_FREEBSD,
Target::X8664UnknownLinuxGnu => X8664_UNKNOWN_LINUX_GNU,
Target::Unknown => "",
};
write!(f, "{}", target_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
struct MockTargetRetriever {
mock_data: Vec<String>,
}
impl MockTargetRetriever {
fn new(mock_data: Vec<String>) -> Self {
Self { mock_data }
}
}
impl TargetRetriever for MockTargetRetriever {
fn get_installed_targets(&self) -> Vec<String> {
self.mock_data.clone()
}
}
#[rstest]
#[case(vec!["".to_string()], false)] // empty string
#[case(vec!["x86_64-pc-windows-msvc".to_string()], false)] // not supported target
#[case(vec!["x86_64-pc-windows-msvc".to_string(), "".to_string()], false)] // not supported target and empty string
#[case(vec!["x86_64-unknown-linux-gnu".to_string()], true)] // one supported target
#[case(vec!["aarch64-apple-darwin".to_string(), "x86_64-unknown-linux-gnu".to_string()], true)] // one unsupported target and one supported
fn test_is_target_supported(#[case] installed_targets: Vec<String>, #[case] expected: bool) {
let mock_retriever = MockTargetRetriever::new(installed_targets);
let sanitizer = Sanitizer::Memory;
assert_eq!(sanitizer.is_target_supported(&mock_retriever), expected);
}
#[test]
fn test_consistency_of_fmt_and_from_str_strings() {
let variants = vec![
Target::Aarch64AppleDarwin,
Target::Aarch64LinuxAndroid,
Target::Aarch64UnknownFuchsia,
Target::Aarch64UnknownLinuxGnu,
Target::X8664AppleDarwin,
Target::X8664LinuxAndroid,
Target::X8664UnknownFuchsia,
Target::X8664UnknownFreebsd,
Target::X8664UnknownLinuxGnu,
];
for variant in variants {
let variant_str = format!("{}", variant);
let parsed_variant = Target::from_str(&variant_str);
assert_eq!(Some(variant), parsed_variant);
}
}
}