1use std::ffi::OsStr;
6use std::path::{Path, PathBuf};
7
8use ignore::WalkBuilder;
9use regex::Regex;
10
11pub const CUSTOM_IGNORE_FILE_NAME: &str = ".cmakefmtignore";
13
14#[derive(Debug, Clone, Default)]
16pub struct DiscoveryOptions<'a> {
17 pub file_filter: Option<&'a Regex>,
19 pub honor_gitignore: bool,
21 pub explicit_ignore_paths: &'a [PathBuf],
23}
24
25pub fn discover_cmake_files(root: &Path, file_filter: Option<&Regex>) -> Vec<PathBuf> {
30 discover_cmake_files_with_options(
31 root,
32 DiscoveryOptions {
33 file_filter,
34 honor_gitignore: false,
35 explicit_ignore_paths: &[],
36 },
37 )
38}
39
40pub fn discover_cmake_files_with_options(
43 root: &Path,
44 options: DiscoveryOptions<'_>,
45) -> Vec<PathBuf> {
46 let mut builder = WalkBuilder::new(root);
47 builder.hidden(false);
48 builder.git_ignore(options.honor_gitignore);
49 builder.git_global(options.honor_gitignore);
50 builder.git_exclude(options.honor_gitignore);
51 builder.require_git(false);
52 builder.add_custom_ignore_filename(CUSTOM_IGNORE_FILE_NAME);
53
54 for ignore_path in options.explicit_ignore_paths {
55 builder.add_ignore(ignore_path);
56 }
57
58 let mut files: Vec<_> = builder
59 .build()
60 .filter_map(Result::ok)
61 .filter(|entry| entry.file_type().is_some_and(|kind| kind.is_file()))
62 .map(|entry| entry.into_path())
63 .filter(|path| is_cmake_file(path))
64 .filter(|path| matches_filter(path, options.file_filter))
65 .collect();
66 files.sort();
67 files
68}
69
70pub fn is_cmake_file(path: &Path) -> bool {
79 let Some(file_name) = path.file_name().and_then(OsStr::to_str) else {
80 return false;
81 };
82
83 if file_name == "CMakeLists.txt" {
84 return true;
85 }
86
87 file_name.ends_with(".cmake") || file_name.ends_with(".cmake.in")
88}
89
90pub fn matches_filter(path: &Path, file_filter: Option<&Regex>) -> bool {
95 let Some(file_filter) = file_filter else {
96 return true;
97 };
98
99 file_filter.is_match(&path.to_string_lossy())
100}