| /********************************************************************* |
| * Copyright (c) 2024 Boeing |
| * |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Boeing - initial API and implementation |
| **********************************************************************/ |
| use anyhow::Context; |
| use applicability_parser::parse_applicability; |
| use applicability_parser_config::{ |
| applic_config::ApplicabilityConfigElement, get_comment_syntax, get_file_contents, |
| }; |
| use applicability_sanitization::SanitizeApplicability; |
| use applicability_substitution::SubstituteApplicability; |
| use clap::Parser; |
| use clap_verbosity_flag::{Verbosity, WarnLevel}; |
| use common_path::common_path; |
| use std::{ |
| fs::{self, create_dir_all, File}, |
| io::ErrorKind, |
| path::{Path, PathBuf}, |
| sync::mpsc::{channel, Receiver}, |
| thread, |
| }; |
| use tracing::{info, Level}; |
| |
| /// Block Applicability Tool(BAT) |
| /// Supported Default Formats: |
| /// *.md Starting Syntax: `` Ending Syntax: `` |
| /// *.cpp Starting Syntax: // Ending Syntax: |
| /// *.cxx Starting Syntax: // Ending Syntax: |
| /// *.cc Starting Syntax: // Ending Syntax: |
| /// *.c Starting Syntax: // Ending Syntax: |
| /// *.hpp Starting Syntax: // Ending Syntax: |
| /// *.hxx Starting Syntax: // Ending Syntax: |
| /// *.hh Starting Syntax: // Ending Syntax: |
| /// *.h Starting Syntax: // Ending Syntax: |
| /// *.rs Starting Syntax: // Ending Syntax: |
| /// *.bzl Starting Syntax: # Ending Syntax: |
| /// *.bazel Starting Syntax: # Ending Syntax: |
| /// WORKSPACE Starting Syntax: # Ending Syntax: |
| /// BUILD Starting Syntax: # Ending Syntax: |
| /// *.fileApplicability Starting Syntax: # Ending Syntax: |
| /// *.applicability Starting Syntax: # Ending Syntax: |
| #[derive(Parser)] |
| #[clap(author = "Luciano Vaglienti", version, verbatim_doc_comment)] |
| struct CliOptions { |
| /// Config file containing the valid applicabilities,configurations, and substitutions. |
| /// An example: |
| ///[ |
| /// { |
| /// "name":"PRODUCT_A", |
| /// "group":"abGroup", |
| /// "features":["ENGINE_5=A2543","JHU_CONTROLLER=Excluded","ROBOT_ARM_LIGHT=Excluded","ROBOT_SPEAKER=SPKR_A"], |
| /// "substitutions":[ |
| /// {"matchText":"SOME_SUBSTITUTION","substitute":"SOME NEW TEXT CONTENT"} |
| /// ] |
| /// }, |
| /// { |
| /// "name":"PRODUCT_B", |
| /// "group":"abGroup", |
| /// "features":["ENGINE_5=A2543","JHU_CONTROLLER=Included","ROBOT_ARM_LIGHT=Included","ROBOT_SPEAKER=SPKR_A"] |
| /// }, |
| /// { |
| /// "name":"abGroup", |
| /// "configs":["PRODUCT_A","PRODUCT_B"], |
| /// "features":["ENGINE_5=A2543","JHU_CONTROLLER=Included","ROBOT_ARM_LIGHT=Included","ROBOT_SPEAKER=SPKR_A"] |
| /// }, |
| /// { |
| /// "name":"PRODUCT_D", |
| /// "group":"", |
| /// "features":["ENGINE_5=B5543","JHU_CONTROLLER=Excluded","ROBOT_ARM_LIGHT=Excluded","ROBOT_SPEAKER=SPKR_B"] |
| /// }, |
| /// { |
| /// "name":"PRODUCT_C", |
| /// "group":"", |
| /// "features":["ENGINE_5=A2543","JHU_CONTROLLER=Included","ROBOT_ARM_LIGHT=Excluded","ROBOT_SPEAKER=SPKR_B"] |
| /// } |
| ///] |
| #[clap(short, long, verbatim_doc_comment)] |
| applicability_config: std::path::PathBuf, |
| |
| /// The output directory for processed files. |
| #[clap(short, long)] |
| out_dir: std::path::PathBuf, |
| |
| /// The input files to pre-process |
| #[clap(short, long, value_delimiter = ',', value_terminator = ";")] |
| srcs: Vec<std::path::PathBuf>, |
| |
| /// Override start comment syntax if the file type is not already natively supported. |
| /// For a C style language, you should opt for // or if you are intending to use multi-line, |
| /// use /* |
| #[clap(short, long, default_value = "//", verbatim_doc_comment)] |
| begin_comment_syntax: String, |
| |
| /// Override end comment syntax if the file type is not already natively supported. |
| /// For a C style language you should not fill this out, unless you are intending to use multi-line, in which case |
| /// you should use */ |
| #[clap(short, long, default_value = None, verbatim_doc_comment)] |
| end_comment_syntax: Option<String>, |
| |
| /// Use output directly as specified instead of looking for a common path |
| #[clap(short, long, verbatim_doc_comment)] |
| use_direct_output: bool, |
| |
| /// Do not write the processed files to a directory in {out_dir}/config/{config_name} |
| #[clap(short, long, verbatim_doc_comment)] |
| no_write_config_folder: bool, |
| |
| ///Verbosity of output, defaults to warnings and errors. |
| /// -q will have no output |
| /// -v will show warnings,info and errors |
| /// -vv will show warnings,info,errors, and debug |
| /// -vvv will show warnings,info,errors, debug and trace output |
| #[command(flatten)] |
| verbose: Verbosity<WarnLevel>, |
| } |
| |
| fn main() { |
| let args = CliOptions::parse(); |
| let handle = std::io::BufWriter::new(std::io::stdout()); |
| let (non_blocking, _guard) = tracing_appender::non_blocking(handle); |
| let subscriber = tracing_subscriber::FmtSubscriber::builder() |
| .with_writer(non_blocking) |
| .with_max_level(match args.verbose.log_level_filter() { |
| clap_verbosity_flag::LevelFilter::Error => Level::ERROR, |
| clap_verbosity_flag::LevelFilter::Warn => Level::WARN, |
| clap_verbosity_flag::LevelFilter::Info => Level::INFO, |
| clap_verbosity_flag::LevelFilter::Debug => Level::DEBUG, |
| clap_verbosity_flag::LevelFilter::Trace => Level::TRACE, |
| clap_verbosity_flag::LevelFilter::Off => Level::ERROR, |
| }) |
| .with_line_number(true) |
| .pretty() |
| .finish(); |
| let _ = tracing::subscriber::set_global_default(subscriber) |
| .map_err(|_err| eprintln!("Unable to set global default subscriber")); |
| let out_dir = args.out_dir.as_path(); |
| let applic_config: Vec<ApplicabilityConfigElement> = match File::open(args.applicability_config) |
| { |
| Ok(file) => match serde_json::from_reader(file) { |
| Ok(res) => res, |
| Err(e) => panic!( |
| "Could not parse applicability config JSON \n{:?}: \tat line {:?} column {:?}", |
| e.classify(), |
| e.line(), |
| e.column() |
| ), |
| }, |
| |
| Err(e) => panic!("Could not find applicability config {:?}", e), |
| }; |
| let start_comment_syntax = args.begin_comment_syntax.as_str(); |
| let end_comment_syntax_temp = match args.end_comment_syntax { |
| Some(i) => i, |
| None => "".to_owned(), |
| }; |
| let end_comment_syntax = end_comment_syntax_temp.as_str(); |
| thread::scope(|scope| { |
| for input in &args.srcs { |
| let applic_config_for_file = applic_config.clone(); |
| let use_direct_output = args.use_direct_output; |
| let should_not_write_config_folder = args.no_write_config_folder; |
| let _outer_thread = scope.spawn(move || { |
| info!("Processing input {}", input.to_str().unwrap_or("")); |
| let file_contents = get_file_contents(input); |
| let (start_syntax, end_syntax) = |
| get_comment_syntax(input, start_comment_syntax, end_comment_syntax); |
| let content_result = |
| parse_applicability(&file_contents, start_syntax.as_str(), end_syntax.as_str()); |
| let contents = match content_result { |
| Ok((_remaining, results)) => results, |
| Err(_) => panic!("Failed to unwrap parsed AST"), |
| }; |
| for config in applic_config_for_file { |
| let copy = contents.clone(); |
| let input_config = config.clone(); |
| let output_config = config.clone(); |
| let (sender, receiver) = channel(); |
| let _s1 = scope.spawn(move || { |
| let substitutions = config.clone().get_substitutions().unwrap_or_default(); |
| let sanitized_content = copy |
| .iter() |
| .cloned() |
| .map(|c| { |
| c.substitute(&substitutions) |
| .sanitize( |
| input_config.clone().get_features(), |
| &input_config.clone().get_name(), |
| &substitutions, |
| config.get_parent_group(), |
| Some(config.get_configs().as_slice()), |
| ) |
| .into() |
| }) |
| .collect::<Vec<String>>() |
| .join(""); |
| sender.send(sanitized_content) |
| }); |
| |
| let _s2 = scope.spawn(move || { |
| output_thread( |
| out_dir, |
| input, |
| should_not_write_config_folder, |
| use_direct_output, |
| output_config, |
| receiver, |
| ) |
| }); |
| } |
| }); |
| } |
| }); |
| } |
| #[tracing::instrument(err)] |
| fn create_starting_output_directory_structure(out_dir: &Path) -> Result<(), anyhow::Error> { |
| create_dir_all(out_dir) |
| .with_context(|| format!("Failed to create output directory {:#?}!", out_dir)) |
| } |
| |
| #[tracing::instrument(err)] |
| fn find_starting_output_directory(out_dir: &Path) -> Result<PathBuf, anyhow::Error> { |
| fs::canonicalize(out_dir) |
| .with_context(|| format!("Error finding output directory {:#?}", out_dir)) |
| } |
| #[tracing::instrument(err)] |
| fn find_starting_input_directory(input: &PathBuf) -> Result<PathBuf, anyhow::Error> { |
| fs::canonicalize(input).with_context(|| { |
| format!( |
| "Error finding input file {:#?} . You should check to see if the file exists.", |
| input |
| ) |
| }) |
| } |
| #[tracing::instrument(err)] |
| fn output_thread( |
| out_dir: &Path, |
| input: &PathBuf, |
| should_not_write_config_folder: bool, |
| use_direct_output: bool, |
| cloned_config: ApplicabilityConfigElement, |
| receiver: Receiver<String>, |
| ) -> Result<(), anyhow::Error> { |
| create_starting_output_directory_structure(out_dir)?; |
| //convert any relative paths to absolute paths |
| let mut out_dirs = find_starting_output_directory(out_dir)?; |
| let input_path = find_starting_input_directory(input)?; |
| let config_path = match should_not_write_config_folder { |
| false => Path::new("config").join(Path::new(&cloned_config.clone().get_name())), |
| true => PathBuf::new(), |
| }; |
| out_dirs.push(config_path); |
| out_dirs.push(match use_direct_output { |
| true => match input.file_name() { |
| Some(file_name) => match file_name.to_str() { |
| Some(i) => i, |
| None => panic!( |
| "Failed to unwrap input file name in direct output mode! {:#?}", |
| file_name |
| ), |
| }, |
| None => panic!( |
| "Failed to unwrap input file name in direct output mode! {:#?}", |
| input |
| ), |
| }, |
| false => match common_path(&input_path, &out_dirs) { |
| Some(prefix) => match input_path.strip_prefix(prefix) { |
| Ok(i) => match i.to_str() { |
| Some(str) => str, |
| None => panic!( |
| "Failed to unwrap input file name in common path mode! {:#?}", |
| i |
| ), |
| }, |
| Err(e) => { |
| println!( |
| "Error stripping input prefix {:?} from input {:?}", |
| e, input |
| ); |
| match input.to_str() { |
| Some(i) => i, |
| None => panic!( |
| "Failed to unwrap input file name in common path mode! {:#?}", |
| input |
| ), |
| } |
| } |
| }, |
| None => panic!( |
| "Error finding the common path between the input {:#?} and output directory {:#?}.", |
| input_path, out_dirs |
| ), |
| }, |
| }); |
| let parent = &out_dirs.parent().unwrap(); |
| let _create_dir_all = create_dir_all(parent); |
| let parent_path_buf = parent.to_path_buf(); |
| let create_directory = &parent_path_buf; |
| create_dir_all(create_directory) |
| .with_context(|| format!("Failed to create directory {:#?}", create_directory))?; |
| let _f = match File::create(&out_dirs) { |
| Ok(fc) => fc, |
| Err(e) => panic!("Problem creating the file: {:?}", e), |
| }; |
| let file_result = File::open(&out_dirs); |
| |
| let _file = match file_result { |
| Ok(file) => file, |
| Err(error) => match error.kind() { |
| ErrorKind::NotFound => match File::create(&out_dirs) { |
| Ok(fc) => fc, |
| Err(e) => panic!("Problem creating the file: {:?}", e), |
| }, |
| other_error => { |
| panic!("Problem opening the file: {:?}", other_error); |
| } |
| }, |
| }; |
| |
| for received in receiver { |
| //write the file out |
| let _text = received.clone(); |
| match fs::write(&out_dirs, received) { |
| Ok(r) => r, |
| Err(e) => println!( |
| "Failed to write {:#?} to {:#?}. \n Error Code: {:#?}", |
| _text, out_dirs, e |
| ), |
| }; |
| } |
| Ok(()) |
| } |