From d9b2d5203266371648b7f58562e585eae1ebde22 Mon Sep 17 00:00:00 2001 From: Laszlo Nagy Date: Tue, 2 Jul 2024 22:42:16 +0200 Subject: [PATCH] rust: create bear module --- rust/Cargo.toml | 3 +- rust/bear/Cargo.toml | 25 +++ rust/bear/src/command.rs | 328 +++++++++++++++++++++++++++++++++ rust/bear/src/configuration.rs | 311 +++++++++++++++++++++++++++++++ rust/bear/src/fixtures.rs | 13 ++ rust/bear/src/main.rs | 64 +++++++ 6 files changed, 743 insertions(+), 1 deletion(-) create mode 100644 rust/bear/Cargo.toml create mode 100644 rust/bear/src/command.rs create mode 100644 rust/bear/src/configuration.rs create mode 100644 rust/bear/src/fixtures.rs create mode 100644 rust/bear/src/main.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c74af6f0..d3a973db 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "semantic", - "intercept" + "intercept", + "bear" ] resolver = "2" diff --git a/rust/bear/Cargo.toml b/rust/bear/Cargo.toml new file mode 100644 index 00000000..770077d6 --- /dev/null +++ b/rust/bear/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bear" +version = "4.0.0" +authors = ["László Nagy "] +description = "Bear is a tool that generates a compilation database for clang tooling." +keywords = ["clang", "clang-tooling", "compilation-database"] +repository = "https://github.com/rizsotto/Bear" +homepage = "https://github.com/rizsotto/Bear" +license = "GPL-3" +edition = "2021" + +[dependencies] +thiserror = "1.0" +anyhow = "1.0" +lazy_static = "1.4" +serde = { version = "1.0", default-features = false, features = ["derive"] } +serde_json = { version = "1.0", default-features = false, features = ["std"] } +clap = { version = "4.5", default-features = false, features = ["std", "cargo", "help", "usage", "suggestions"] } +chrono = "0.4.33" +log = "0.4" +simple_logger = { version = "5.0", default-features = false, features = ["timestamps"]} + +[[bin]] +name = "bear" +path = "src/main.rs" diff --git a/rust/bear/src/command.rs b/rust/bear/src/command.rs new file mode 100644 index 00000000..4e18f3e5 --- /dev/null +++ b/rust/bear/src/command.rs @@ -0,0 +1,328 @@ +/* Copyright (C) 2012-2024 by László Nagy + This file is part of Bear. + + Bear is a tool to generate compilation database for clang tooling. + + Bear is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bear is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +//! This module contains the command line interface of the application. +//! +//! The command line parsing is implemented using the `clap` library. +//! The module is defining types to represent a structured form of the +//! program invocation. The `Arguments` type is used to represent all +//! possible invocations of the program. +//! +//! # Example +//! +//! ```rust +//! let matches = cli().get_matches(); +//! let arguments = Arguments::try_from(matches)?; +//! ``` + +use anyhow::anyhow; +use clap::{arg, ArgAction, ArgMatches, command, Command}; + +/// Common constants used in the module. +const MODE_INTERCEPT_SUBCOMMAND: &str = "intercept"; +const MODE_SEMANTIC_SUBCOMMAND: &str = "semantic"; +const DEFAULT_OUTPUT_FILE: &str = "compile_commands.json"; +const DEFAULT_EVENT_FILE: &str = "events.json"; + +/// Represents the command line arguments of the application. +#[derive(Debug, PartialOrd, PartialEq)] +pub(crate) struct Arguments { + // The verbosity level of the application. + pub verbose: u8, + // The path of the configuration file. + pub config: Option, + // The mode of the application. + pub mode: Mode, +} + +/// Represents the execution of a command. +#[derive(Debug, PartialOrd, PartialEq)] +pub(crate) struct BuildCommand { + arguments: Vec, +} + +#[derive(Debug, PartialOrd, PartialEq)] +pub(crate) struct BuildSemantic { + file_name: String, + append: bool, +} + +#[derive(Debug, PartialOrd, PartialEq)] +pub(crate) struct BuildEvents { + file_name: String, +} + +/// Represents the mode of the application. +#[derive(Debug, PartialOrd, PartialEq)] +pub(crate) enum Mode { + Intercept { + input: BuildCommand, + output: BuildEvents, + }, + Semantic { + input: BuildEvents, + output: BuildSemantic, + }, + All { + input: BuildCommand, + output: BuildSemantic, + }, +} + + +impl TryFrom for Arguments { + type Error = anyhow::Error; + + fn try_from(matches: ArgMatches) -> Result { + let verbose = matches.get_count("verbose"); + let config = matches.get_one::("config") + .map(String::to_string); + + match matches.subcommand() { + Some((MODE_INTERCEPT_SUBCOMMAND, intercept_matches)) => { + let input = BuildCommand::try_from(intercept_matches)?; + let output = intercept_matches.get_one::("output") + .map(String::to_string) + .expect("output is defaulted"); + + // let output = BuildEvents::try_from(intercept_matches)?; + let mode = Mode::Intercept { input, output: BuildEvents { file_name: output } }; + let arguments = Arguments { verbose, config, mode }; + Ok(arguments) + } + Some((MODE_SEMANTIC_SUBCOMMAND, semantic_matches)) => { + let input = semantic_matches.get_one::("input") + .map(String::to_string) + .expect("input is defaulted"); + + let output = BuildSemantic::try_from(semantic_matches)?; + let mode = Mode::Semantic { input: BuildEvents { file_name: input }, output }; + let arguments = Arguments { verbose, config, mode }; + Ok(arguments) + } + None => { + let input = BuildCommand::try_from(&matches)?; + let output = BuildSemantic::try_from(&matches)?; + let mode = Mode::All { input, output }; + let arguments = Arguments { verbose, config, mode }; + Ok(arguments) + } + _ => { + Err(anyhow!("unrecognized subcommand")) + } + } + } +} + +impl TryFrom<&ArgMatches> for BuildCommand { + type Error = anyhow::Error; + + fn try_from(matches: &ArgMatches) -> Result { + let arguments = matches.get_many("COMMAND") + .expect("missing build command") + .cloned() + .collect(); + Ok(BuildCommand { arguments }) + } +} + +impl TryFrom<&ArgMatches> for BuildSemantic { + type Error = anyhow::Error; + + fn try_from(matches: &ArgMatches) -> Result { + let file_name = matches.get_one::("output") + .map(String::to_string) + .expect("output is defaulted"); + let append = *matches.get_one::("append") + .unwrap_or(&false); + Ok(BuildSemantic { file_name, append }) + } +} + + +/// Represents the command line interface of the application. +/// +/// This describes how the user can interact with the application. +/// The different modes of the application are represented as subcommands. +/// The application can be run in intercept mode, semantic mode, or the +/// default mode where both intercept and semantic are executed. +pub(crate) fn cli() -> Command { + command!() + .subcommand_required(false) + .subcommand_negates_reqs(true) + .subcommand_precedence_over_arg(true) + .arg_required_else_help(true) + .args(&[ + arg!(-v --verbose ... "Sets the level of verbosity") + .action(ArgAction::Count), + arg!(-c --config "Path of the config file"), + ]) + .subcommand( + Command::new(MODE_INTERCEPT_SUBCOMMAND) + .about("intercepts command execution") + .args(&[ + arg!( "Build command") + .action(ArgAction::Append) + .value_terminator("--") + .num_args(1..) + .last(true) + .required(true), + arg!(-o --output "Path of the event file") + .default_value(DEFAULT_EVENT_FILE) + .hide_default_value(false), + ]) + .arg_required_else_help(true), + ) + .subcommand( + Command::new(MODE_SEMANTIC_SUBCOMMAND) + .about("detect semantics of command executions") + .args(&[ + arg!(-i --input "Path of the event file") + .default_value(DEFAULT_EVENT_FILE) + .hide_default_value(false), + arg!(-o --output "Path of the result file") + .default_value(DEFAULT_OUTPUT_FILE) + .hide_default_value(false), + arg!(-a --append "Append result to an existing output file") + .action(ArgAction::SetTrue), + ]) + .arg_required_else_help(false), + ) + .args(&[ + arg!( "Build command") + .action(ArgAction::Append) + .value_terminator("--") + .num_args(1..) + .last(true) + .required(true), + arg!(-o --output "Path of the result file") + .default_value(DEFAULT_OUTPUT_FILE) + .hide_default_value(false), + arg!(-a --append "Append result to an existing output file") + .action(ArgAction::SetTrue), + ]) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::vec_of_strings; + + #[test] + fn test_intercept_call() { + let execution = vec!["bear", "-vvv", "-c", "~/bear.yaml", "intercept", "-o", "custom.json", "--", "make", "all"]; + + let matches = cli().get_matches_from(execution); + let arguments = Arguments::try_from(matches).unwrap(); + + assert_eq!(arguments, Arguments { + verbose: 3, + config: Some("~/bear.yaml".to_string()), + mode: Mode::Intercept { + input: BuildCommand { arguments: vec_of_strings!["make", "all"] }, + output: BuildEvents { file_name: "custom.json".to_string() }, + }, + }); + } + + #[test] + fn test_intercept_defaults() { + let execution = vec!["bear", "intercept", "--", "make", "all"]; + + let matches = cli().get_matches_from(execution); + let arguments = Arguments::try_from(matches).unwrap(); + + assert_eq!(arguments, Arguments { + verbose: 0, + config: None, + mode: Mode::Intercept { + input: BuildCommand { arguments: vec_of_strings!["make", "all"] }, + output: BuildEvents { file_name: "events.json".to_string() }, + }, + }); + } + + #[test] + fn test_semantic_call() { + let execution = vec!["bear", "-vvv", "-c", "~/bear.yaml", "semantic", "-i", "custom.json", "-o", "result.json", "-a"]; + + let matches = cli().get_matches_from(execution); + let arguments = Arguments::try_from(matches).unwrap(); + + assert_eq!(arguments, Arguments { + verbose: 3, + config: Some("~/bear.yaml".to_string()), + mode: Mode::Semantic { + input: BuildEvents { file_name: "custom.json".to_string() }, + output: BuildSemantic { file_name: "result.json".to_string(), append: true }, + }, + }); + } + + #[test] + fn test_semantic_defaults() { + let execution = vec!["bear", "semantic"]; + + let matches = cli().get_matches_from(execution); + let arguments = Arguments::try_from(matches).unwrap(); + + assert_eq!(arguments, Arguments { + verbose: 0, + config: None, + mode: Mode::Semantic { + input: BuildEvents { file_name: "events.json".to_string() }, + output: BuildSemantic { file_name: "compile_commands.json".to_string(), append: false }, + }, + }); + } + + #[test] + fn test_all_call() { + let execution = vec!["bear", "-vvv", "-c", "~/bear.yaml", "-o", "result.json", "-a", "--", "make", "all"]; + + let matches = cli().get_matches_from(execution); + let arguments = Arguments::try_from(matches).unwrap(); + + assert_eq!(arguments, Arguments { + verbose: 3, + config: Some("~/bear.yaml".to_string()), + mode: Mode::All { + input: BuildCommand { arguments: vec_of_strings!["make", "all"] }, + output: BuildSemantic { file_name: "result.json".to_string(), append: true }, + }, + }); + } + + #[test] + fn test_all_defaults() { + let execution = vec!["bear", "--", "make", "all"]; + + let matches = cli().get_matches_from(execution); + let arguments = Arguments::try_from(matches).unwrap(); + + assert_eq!(arguments, Arguments { + verbose: 0, + config: None, + mode: Mode::All { + input: BuildCommand { arguments: vec_of_strings!["make", "all"] }, + output: BuildSemantic { file_name: "compile_commands.json".to_string(), append: false }, + }, + }); + } +} \ No newline at end of file diff --git a/rust/bear/src/configuration.rs b/rust/bear/src/configuration.rs new file mode 100644 index 00000000..d9416c9b --- /dev/null +++ b/rust/bear/src/configuration.rs @@ -0,0 +1,311 @@ +/* Copyright (C) 2012-2024 by László Nagy + This file is part of Bear. + + Bear is a tool to generate compilation database for clang tooling. + + Bear is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bear is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +use std::path::PathBuf; + +use serde::Deserialize; + +// Represents the application configuration. +#[derive(Debug, Default, Deserialize, PartialEq)] +pub struct Configuration { + #[serde(default)] + pub output: Output, + #[serde(default)] + pub compilation: Compilation, +} + +// Represents compiler related configuration. +#[derive(Debug, Default, Deserialize, PartialEq)] +pub struct Compilation { + #[serde(default)] + pub compilers_to_recognize: Vec, + #[serde(default)] + pub compilers_to_exclude: Vec, +} + +// Represents a compiler wrapper that the tool will recognize. +// +// When executable name matches it tries to parse the flags as it would +// be a known compiler, and append the additional flags to the output +// entry if the compiler is recognized. +#[derive(Debug, Deserialize, PartialEq)] +pub struct CompilerToRecognize { + pub executable: PathBuf, + #[serde(default)] + pub flags_to_add: Vec, + #[serde(default)] + pub flags_to_remove: Vec, +} + +// Groups together the output related configurations. +#[derive(Debug, Default, Deserialize, PartialEq)] +pub struct Output { + #[serde(default)] + pub format: Format, + #[serde(default)] + pub content: Content, +} + +// Controls the output format. +// +// The entries in the JSON compilation database can have different forms. +// One format element is how the command is represented: it can be an array +// of strings or a single string (shell escaping to protect white spaces). +// Another format element is if the output field is emitted or not. +#[derive(Debug, Deserialize, PartialEq)] +pub struct Format { + #[serde(default = "enabled")] + pub command_as_array: bool, + #[serde(default = "disabled")] + pub drop_output_field: bool, +} + +impl Default for Format { + fn default() -> Self { + Format { + command_as_array: enabled(), + drop_output_field: disabled(), + } + } +} + +// Controls the content of the output. +// +// This will act as a filter on the output elements. +// These attributes can be read from the configuration file, and can be +// overridden by command line arguments. +#[derive(Debug, Deserialize, PartialEq)] +pub struct Content { + #[serde(default = "disabled")] + pub include_only_existing_source: bool, + #[serde(default)] + pub duplicate_filter_fields: DuplicateFilterFields, + #[serde(default)] + pub paths_to_include: Vec, + #[serde(default)] + pub paths_to_exclude: Vec, +} + +impl Default for Content { + fn default() -> Self { + Content { + include_only_existing_source: disabled(), + duplicate_filter_fields: DuplicateFilterFields::default(), + paths_to_include: vec![], + paths_to_exclude: vec![], + } + } +} + +fn disabled() -> bool { + false +} + +fn enabled() -> bool { + true +} + +/// Represents how the duplicate filtering detects duplicate entries. +#[derive(Debug, Default, Deserialize, PartialEq)] +#[serde(try_from = "String")] +pub enum DuplicateFilterFields { + FileOnly, + #[default] + FileAndOutputOnly, + All, +} + +impl TryFrom for DuplicateFilterFields { + type Error = String; + + fn try_from(value: String) -> Result { + match value.as_str() { + "file" => + Ok(DuplicateFilterFields::FileOnly), + "file_output" => + Ok(DuplicateFilterFields::FileAndOutputOnly), + "all" => + Ok(DuplicateFilterFields::All), + _ => + Err(format!(r#"Unknown value "{value}" for duplicate filter"#)), + } + } +} + +#[cfg(test)] +mod test { + use crate::{vec_of_pathbuf, vec_of_strings}; + use super::*; + + #[test] + fn test_full_config() { + let content: &[u8] = br#"{ + "output": { + "format": { + "command_as_array": true, + "drop_output_field": false + }, + "content": { + "include_only_existing_source": false, + "duplicate_filter_fields": "all", + "paths_to_include": ["sources"], + "paths_to_exclude": ["tests"] + } + }, + "compilation": { + "compilers_to_recognize": [ + { + "executable": "/usr/local/bin/clang", + "flags_to_add": ["-Dfoo=bar"], + "flags_to_remove": ["-Wall"] + } + ], + "compilers_to_exclude": [ + "clang" + ] + } + }"#; + + let result = serde_json::from_reader(content).unwrap(); + + let expected = Configuration { + output: Output { + format: Format { + command_as_array: true, + drop_output_field: false, + }, + content: Content { + include_only_existing_source: false, + duplicate_filter_fields: DuplicateFilterFields::All, + paths_to_include: vec_of_pathbuf!["sources"], + paths_to_exclude: vec_of_pathbuf!["tests"], + }, + }, + compilation: Compilation { + compilers_to_recognize: vec![ + CompilerToRecognize { + executable: PathBuf::from("/usr/local/bin/clang"), + flags_to_add: vec_of_strings!["-Dfoo=bar"], + flags_to_remove: vec_of_strings!["-Wall"], + } + ], + compilers_to_exclude: vec_of_pathbuf!["clang"], + }, + }; + + assert_eq!(expected, result); + } + + #[test] + fn test_only_output_config() { + let content: &[u8] = br#"{ + "output": { + "format": { + "command_as_array": false + }, + "content": { + "duplicate_filter_fields": "file" + } + } + }"#; + + let result = serde_json::from_reader(content).unwrap(); + + let expected = Configuration { + output: Output { + format: Format { + command_as_array: false, + drop_output_field: false, + }, + content: Content { + include_only_existing_source: false, + duplicate_filter_fields: DuplicateFilterFields::FileOnly, + paths_to_include: vec_of_pathbuf![], + paths_to_exclude: vec_of_pathbuf![], + }, + }, + compilation: Compilation::default(), + }; + + assert_eq!(expected, result); + } + + #[test] + fn test_compilation_only_config() { + let content: &[u8] = br#"{ + "compilation": { + "compilers_to_recognize": [ + { + "executable": "/usr/local/bin/clang" + }, + { + "executable": "/usr/local/bin/clang++" + } + ], + "compilers_to_exclude": [ + "clang", "clang++" + ] + } + }"#; + + let result = serde_json::from_reader(content).unwrap(); + + let expected = Configuration { + output: Output::default(), + compilation: Compilation { + compilers_to_recognize: vec![ + CompilerToRecognize { + executable: PathBuf::from("/usr/local/bin/clang"), + flags_to_add: vec![], + flags_to_remove: vec![], + }, + CompilerToRecognize { + executable: PathBuf::from("/usr/local/bin/clang++"), + flags_to_add: vec![], + flags_to_remove: vec![], + }, + ], + compilers_to_exclude: vec_of_pathbuf!["clang", "clang++"], + }, + }; + + assert_eq!(expected, result); + } + + #[test] + fn test_failing_config() { + let content: &[u8] = br#"{ + "output": { + "format": { + "command_as_array": false + }, + "content": { + "duplicate_filter_fields": "files" + } + } + }"#; + + let result: Result = serde_json::from_reader(content); + + assert!(result.is_err()); + + let message = result.unwrap_err().to_string(); + assert_eq!("Unknown value \"files\" for duplicate filter at line 8 column 17", message); + } +} \ No newline at end of file diff --git a/rust/bear/src/fixtures.rs b/rust/bear/src/fixtures.rs new file mode 100644 index 00000000..9099fff8 --- /dev/null +++ b/rust/bear/src/fixtures.rs @@ -0,0 +1,13 @@ + +#[cfg(test)] +mod fixtures { + #[macro_export] + macro_rules! vec_of_strings { + ($($x:expr),*) => (vec![$($x.to_string()),*]); + } + + #[macro_export] + macro_rules! vec_of_pathbuf { + ($($x:expr),*) => (vec![$(PathBuf::from($x)),*]); + } +} diff --git a/rust/bear/src/main.rs b/rust/bear/src/main.rs new file mode 100644 index 00000000..ece91bcd --- /dev/null +++ b/rust/bear/src/main.rs @@ -0,0 +1,64 @@ +/* Copyright (C) 2012-2024 by László Nagy + This file is part of Bear. + + Bear is a tool to generate compilation database for clang tooling. + + Bear is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bear is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +use log::{debug, LevelFilter}; +use simple_logger::SimpleLogger; + +mod command; +mod configuration; +mod fixtures; + + +/// Driver function of the application. +fn main() -> anyhow::Result<()> { + let matches = command::cli().get_matches(); + let arguments = command::Arguments::try_from(matches)?; + prepare_logging(arguments.verbose)?; + + debug!("Arguments: {:?}", arguments); + + return Ok(()); +} + +/// Initializes the logging system. +/// +/// # Arguments +/// +/// * `level` - The verbosity level of the logging system. +/// +/// # Returns +/// +/// Failure when the downstream library fails to initialize the logging system. +fn prepare_logging(level: u8) -> anyhow::Result<()> { + let level = match level { + 0 => LevelFilter::Error, + 1 => LevelFilter::Warn, + 2 => LevelFilter::Info, + 3 => LevelFilter::Debug, + _ => LevelFilter::Trace, + }; + let mut logger = SimpleLogger::new() + .with_level(level); + if level <= LevelFilter::Debug { + logger = logger.with_local_timestamps() + } + logger.init()?; + + Ok(()) +}