Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Do not build glob matchers repeatedly when include-exclude feature is enabled #244

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use rust_embed_utils::PathMatcher;
use std::{
collections::BTreeMap,
env,
Expand All @@ -25,7 +26,8 @@ fn embedded(

let includes: Vec<&str> = includes.iter().map(AsRef::as_ref).collect();
let excludes: Vec<&str> = excludes.iter().map(AsRef::as_ref).collect();
for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in rust_embed_utils::get_files(absolute_folder_path.clone(), &includes, &excludes) {
let matcher = PathMatcher::new(&includes, &excludes);
for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in rust_embed_utils::get_files(absolute_folder_path.clone(), matcher) {
match_values.insert(
rel_path.clone(),
embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only)?,
Expand Down Expand Up @@ -125,8 +127,8 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
const EXCLUDES: &[&str] = &[#(#excludes),*];
};

// In metadata_only mode, we still need to read file contents to generate the file hash, but
// then we drop the file data.
// In metadata_only mode, we still need to read file contents to generate the
// file hash, but then we drop the file data.
let strip_contents = metadata_only.then_some(quote! {
.map(|mut file| { file.data = ::std::default::Default::default(); file })
});
Expand All @@ -137,13 +139,18 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
quote! {
#[cfg(debug_assertions)]
impl #ident {


fn matcher() -> ::rust_embed::utils::PathMatcher {
#declare_includes
#declare_excludes
static PATH_MATCHER: ::std::sync::OnceLock<::rust_embed::utils::PathMatcher> = ::std::sync::OnceLock::new();
PATH_MATCHER.get_or_init(|| rust_embed::utils::PathMatcher::new(INCLUDES, EXCLUDES)).clone()
}
/// Get an embedded file and its metadata.
pub fn get(file_path: &str) -> ::std::option::Option<rust_embed::EmbeddedFile> {
#handle_prefix

#declare_includes
#declare_excludes

let rel_file_path = file_path.replace("\\", "/");
let file_path = ::std::path::Path::new(#folder_path).join(&rel_file_path);

Expand All @@ -162,8 +169,8 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
return ::std::option::Option::None;
}
}

if rust_embed::utils::is_path_included(&rel_file_path, INCLUDES, EXCLUDES) {
let path_matcher = Self::matcher();
if path_matcher.is_path_included(&rel_file_path) {
rust_embed::utils::read_file_from_fs(&canonical_file_path).ok() #strip_contents
} else {
::std::option::Option::None
Expand All @@ -174,10 +181,8 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
pub fn iter() -> impl ::std::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> {
use ::std::path::Path;

#declare_includes
#declare_excludes

rust_embed::utils::get_files(::std::string::String::from(#folder_path), INCLUDES, EXCLUDES)
rust_embed::utils::get_files(::std::string::String::from(#folder_path), Self::matcher())
.map(|e| #map_iter)
}
}
Expand Down
91 changes: 49 additions & 42 deletions utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,8 @@ pub struct FileEntry {
pub full_canonical_path: String,
}

#[cfg(not(feature = "include-exclude"))]
pub fn is_path_included(_path: &str, _includes: &[&str], _excludes: &[&str]) -> bool {
true
}

#[cfg(feature = "include-exclude")]
pub fn is_path_included(rel_path: &str, includes: &[&str], excludes: &[&str]) -> bool {
use globset::Glob;

// ignore path matched by exclusion pattern
for exclude in excludes {
let pattern = Glob::new(exclude)
.unwrap_or_else(|_| panic!("invalid exclude pattern '{}'", exclude))
.compile_matcher();

if pattern.is_match(rel_path) {
return false;
}
}

// accept path if no includes provided
if includes.is_empty() {
return true;
}

// accept path if matched by inclusion pattern
for include in includes {
let pattern = Glob::new(include)
.unwrap_or_else(|_| panic!("invalid include pattern '{}'", include))
.compile_matcher();

if pattern.is_match(rel_path) {
return true;
}
}

false
}

#[cfg_attr(all(debug_assertions, not(feature = "debug-embed")), allow(unused))]
pub fn get_files<'patterns>(folder_path: String, includes: &'patterns [&str], excludes: &'patterns [&str]) -> impl Iterator<Item = FileEntry> + 'patterns {
pub fn get_files(folder_path: String, matcher: PathMatcher) -> impl Iterator<Item = FileEntry> {
walkdir::WalkDir::new(&folder_path)
.follow_links(true)
.sort_by_file_name()
Expand All @@ -68,8 +29,7 @@ pub fn get_files<'patterns>(folder_path: String, includes: &'patterns [&str], ex
} else {
rel_path
};

if is_path_included(&rel_path, includes, excludes) {
if matcher.is_path_included(&rel_path) {
Some(FileEntry { rel_path, full_canonical_path })
} else {
None
Expand Down Expand Up @@ -176,3 +136,50 @@ pub fn read_file_from_fs(file_path: &Path) -> io::Result<EmbeddedFile> {
fn path_to_str<P: AsRef<std::path::Path>>(p: P) -> String {
p.as_ref().to_str().expect("Path does not have a string representation").to_owned()
}

#[derive(Clone)]
pub struct PathMatcher {
#[cfg(feature = "include-exclude")]
include_matcher: globset::GlobSet,
#[cfg(feature = "include-exclude")]
exclude_matcher: globset::GlobSet,
}

#[cfg(feature = "include-exclude")]
impl PathMatcher {
pub fn new(includes: &[&str], excludes: &[&str]) -> Self {
let mut include_matcher = globset::GlobSetBuilder::new();
for include in includes {
include_matcher.add(globset::Glob::new(include).unwrap_or_else(|_| panic!("invalid include pattern '{}'", include)));
}
let include_matcher = include_matcher
.build()
.unwrap_or_else(|_| panic!("Could not compile included patterns matcher"));

let mut exclude_matcher = globset::GlobSetBuilder::new();
for exclude in excludes {
exclude_matcher.add(globset::Glob::new(exclude).unwrap_or_else(|_| panic!("invalid exclude pattern '{}'", exclude)));
}
let exclude_matcher = exclude_matcher
.build()
.unwrap_or_else(|_| panic!("Could not compile excluded patterns matcher"));

Self {
include_matcher,
exclude_matcher,
}
}
pub fn is_path_included(&self, path: &str) -> bool {
!self.exclude_matcher.is_match(path) && (self.include_matcher.is_empty() || self.include_matcher.is_match(path))
}
}

#[cfg(not(feature = "include-exclude"))]
impl PathMatcher {
pub fn new(_includes: &[&str], _excludes: &[&str]) -> Self {
Self {}
}
pub fn is_path_included(&self, _path: &str) -> bool {
true
}
}
Loading