uvconvertor 0.1.0

A tool generate compile_commands.json from keilMDKs' .uvprojx file
mod uvconvertor;
use std::{collections::HashSet, fs::{self, File}, io::stdout, path::PathBuf};
use clap::Parser;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use uvconvertor::UVConvertor;

#[derive(Parser, Debug)]
#[command(
    version = "1.0",
    author = "Jie",
    about = "A tool generate compile_commands.json from keilMDKs' .uvprojx file",
    long_about = "This project is a reimplementation of uvConvertor in Rust from https://github.com/vankubo/uvConvertor.git, with many additional features. It can parse one or more Keil MDK project's engineering files .uvprojx and the .dep files generated by this project, read the compilation commands of the source files in them, then combine the compilation commands of each project and generate a compile_commands.json file. This file can make Keil MDK's projects be indexed by clangd, which can help editors that support Language Service Protocol (e.g. VSCode, NeoVim, CLion) implement functions such as code hints and code error diagnosis."
)]
struct CliArgs {
    /// Input files with names of targets. First value is file path, subsequent values are names.
    /// Format: -f <path>[,target1[,target2[...]]] OR multiple: -f <file1> -f <file2>
    #[arg(
        short = 'f',
        long = "file",
        required = true,
        value_name = "FILE_WITH_TARGETS",
        num_args = 1..,
    )]
    file_args: Vec<String>,

    /// The directory where the output file compile_commands.json is located.
    /// Default export to stdout
    #[arg(short = 'o', long = "output", value_name = "OUTPUT_DIRECTORY")]
    output: Option<PathBuf>,

    /// Additional arguments
    #[arg(
        short = 'e',
        long = "extopts",
        value_name = "ARG",
        num_args = 0..,
        value_delimiter=','
    )]
    extopts: Vec<String>,

    /// Arguments to remove
    #[arg(
        short = 'r',
        long = "rmopts",
        value_name = "ARG",
        num_args = 0..,
        value_delimiter=','
    )]
    rmopts: Vec<String>,

    /// This pattern will be used to replace disks of all absolute paths.
    /// e.g. pattern "/mnt/$disk" can replace "C:/.../incldue" to "/mnt/c/.../incldue"
    #[arg(short = 'p', long = "pattern", value_name = "PATTERN")]
    pattern: Option<String>,

    /// Exclude sysroot's include paths
    #[arg(short = 'n', long = "no-sysinc")]
    no_sysinc: bool,
}

/// 解析后的文件参数
#[derive(Debug)]
struct FileWithTargets {
    path: PathBuf,
    targets: Vec<Option<String>>,
}

/// 解析命令行参数后得到的结构化数据
#[derive(Debug)]
struct ProcessedArgs {
    files: Vec<FileWithTargets>,
    output: Option<PathBuf>,
    extra_args: Vec<String>,
    removed_args: Vec<String>,
    disk_repl: Option<String>,
    without_sysroot: bool,
}

impl CliArgs {
    /// 解析文件参数为结构化格式
    fn parse_file_args(&self) -> Vec<FileWithTargets> {
        self.file_args.iter()
            .filter_map(|arg| {
                let parts: Vec<&str> = arg.split(',').collect();                
                let path = PathBuf::from(parts[0].trim());
                let targets: Vec<_> = parts[1..].iter()
                    .map(|s| s.trim().to_string()).collect::<HashSet<_>>()
                    .into_iter().map(Some).collect();
                Some(FileWithTargets {
                    path,
                    targets: targets.is_empty().then(|| vec![None]).unwrap_or(targets),
                })
            })
            .collect()
    }
    
    /// 转换为最终处理用的参数结构
    fn to_processed_args(&self) -> ProcessedArgs {
        ProcessedArgs {
            files: self.parse_file_args(),
            output: self.output.clone(),
            extra_args: self.extopts.clone(),
            removed_args: self.rmopts.clone(),
            disk_repl: self.pattern.clone(),
            without_sysroot: self.no_sysinc,
        }
    }
}

fn main() {
    let cli_args = CliArgs::parse();
    let args = cli_args.to_processed_args();

    let mut convertor = args.files.par_iter().map(|f| {
        f.targets.par_iter().filter_map(|t| {
            match UVConvertor::from(&f.path, t.as_deref()) {
                Ok(convertor) => Some(convertor),
                Err(e) => {
                    eprintln!("{e}");
                    None
                },
            }
        }).reduce(UVConvertor::new, |a, b| a + b)
    }).reduce(UVConvertor::new, |a, b| a + b);

    if let Some(rep) = args.disk_repl {
        convertor.replace_disk(&rep);
    }

    if args.without_sysroot {
        convertor.remove_sysroot();
    }

    convertor.remove_arguments(&args.removed_args);
    convertor.add_arguments(&args.extra_args);

    if let Some(outdir) = args.output {
        fs::create_dir_all(&outdir).unwrap();
        let output = outdir.join("compile_commands.json");
        convertor.dump_to_json(File::create(output).unwrap()).unwrap();
    } else {
        convertor.dump_to_json(stdout()).unwrap();
    }
}