use std::{
cell::RefCell,
collections::{HashMap, HashSet},
fs,
path::{Path, PathBuf},
};
use itertools::Itertools;
use roxygen::roxygen;
use crate::{
api::{batching::cfg_filter, record::Record, utils::jsonl::read_jsonl_file},
SourceLocation, TargetTriple, CRATE_NAME_FILE, FEATURES_FILE,
};
const JSONL_EXTENSION: &str = ".jsonl";
thread_local! {
static DOCTEST_SOURCE: RefCell<Option<Source>> = const { RefCell::new(None) };
}
#[derive(Clone)]
pub struct Source {
crate_name: String,
items: HashMap<String, Vec<(syn::Item, SourceLocation)>>,
features_constant: Option<String>,
target_triple: Option<String>,
features_list: Vec<String>, }
impl Source {
#[roxygen]
pub fn new<P: AsRef<Path>>(
input_dir: P,
) -> Self {
Self::builder(input_dir).build()
}
pub fn builder<P: AsRef<Path>>(input_dir: P) -> Builder {
Builder::new(input_dir)
}
}
impl Source {
fn build_internal(
input_dir: &Path,
features_constant: Option<String>,
target_triple: Option<String>,
crate_name_override: Option<String>,
) -> Self {
if let Some(source) = DOCTEST_SOURCE.with(|source| (*source.borrow()).clone()) {
return source;
}
if !input_dir.is_dir() {
panic!(
"Input directory {} does not exist or is not a directory",
input_dir.display()
);
}
let stored_crate_name = read_stored_crate_name(input_dir).unwrap_or_else(|| {
panic!(
"The directory {} was not initialized with init_prebindgen_out_dir(). \
Please ensure that init_prebindgen_out_dir() is called in the build.rs of the source crate.",
input_dir.display()
)
});
let crate_name = crate_name_override.unwrap_or(stored_crate_name);
let groups = Self::discover_groups(input_dir);
let mut items = HashMap::new();
for group in groups {
let records = Self::read_group(input_dir, &group);
let group_items = records
.iter()
.map(|r| {
let (item, mut loc) = r.parse();
loc.crate_name = Some(crate_name.clone());
(item, loc)
})
.collect::<Vec<_>>();
items.insert(group, group_items);
}
let features_list = read_features_from_out_dir(input_dir);
Self {
crate_name,
items,
features_constant,
features_list,
target_triple,
}
}
#[doc(hidden)]
pub fn init_doctest_simulate() {
let source = Self {
crate_name: "source_ffi".to_string(),
items: HashMap::from([
(
"structs".to_string(),
vec![(
syn::parse_quote! {
#[prebindgen("structs")]
pub struct TestStruct {
pub field: i32,
}
},
SourceLocation {
crate_name: Some("source_ffi".to_string()),
..SourceLocation::default()
},
)],
),
(
"functions".to_string(),
vec![(
syn::parse_quote! {
#[prebindgen("functions")]
pub fn test_function() -> i32 { 42 }
},
SourceLocation {
crate_name: Some("source_ffi".to_string()),
..SourceLocation::default()
},
)],
),
]),
features_constant: None,
target_triple: None,
features_list: Vec::new(),
};
DOCTEST_SOURCE.with(|cell| {
*cell.borrow_mut() = Some(source);
});
}
pub fn crate_name(&self) -> &str {
&self.crate_name
}
pub fn items_in_groups<'a>(
&'a self,
groups: &'a [&'a str],
) -> impl Iterator<Item = (syn::Item, SourceLocation)> + 'a {
let mut filter = self.build_cfg_filter();
groups
.iter()
.filter_map(|group| self.items.get(*group))
.flat_map(|records| records.iter().cloned())
.batching(move |iter| filter.call(iter))
}
pub fn items_except_groups<'a>(
&'a self,
groups: &'a [&'a str],
) -> impl Iterator<Item = (syn::Item, SourceLocation)> + 'a {
let mut filter = self.build_cfg_filter();
self.items
.iter()
.filter(|(group, _)| !groups.contains(&group.as_str()))
.flat_map(|(_, records)| records.iter().cloned())
.batching(move |iter| filter.call(iter))
}
pub fn items_all(&self) -> impl Iterator<Item = (syn::Item, SourceLocation)> + '_ {
let mut filter = self.build_cfg_filter();
self.items
.values()
.flat_map(|records| records.iter().cloned())
.batching(move |iter| filter.call(iter))
}
fn build_cfg_filter(&self) -> cfg_filter::CfgFilter {
let mut builder = cfg_filter::CfgFilter::builder();
if let Some(const_name) = &self.features_constant {
let qualified_const = if const_name.contains("::") {
const_name.clone()
} else {
format!("{}::{}", self.crate_name.replace('-', "_"), const_name)
};
let features_list = self
.features_list
.iter()
.map(|f| format!("{}/{}", self.crate_name, f))
.join(" ");
builder = builder.predefined_features(qualified_const, features_list);
}
if let Some(target) = &self.target_triple {
let target_triple = TargetTriple::parse(target)
.unwrap_or_else(|e| panic!("Failed to parse target triple '{}': {}", target, e));
if let Some(arch) = target_triple.arch() {
builder = builder.enable_target_arch(arch);
}
if let Some(vendor) = target_triple.vendor() {
builder = builder.enable_target_vendor(vendor);
}
if let Some(os) = target_triple.os() {
builder = builder.enable_target_os(os);
}
if let Some(env) = target_triple.env() {
builder = builder.enable_target_env(env);
}
}
builder.build()
}
fn read_group<P: AsRef<Path>>(input_dir: P, group: &str) -> Vec<Record> {
let pattern = format!("{group}_");
let mut record_map = HashMap::new();
if let Ok(entries) = fs::read_dir(&input_dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.starts_with(&pattern) && file_name.ends_with(JSONL_EXTENSION) {
#[cfg(feature = "debug")]
println!("Reading exported file: {}", path.display());
let path_clone = path.clone();
match read_jsonl_file(&path) {
Ok(records) => {
for record in records {
let key = if let Some(cfg) = &record.cfg {
format!("{}#{}", record.name, cfg)
} else {
record.name.clone()
};
record_map.insert(key, record);
}
}
Err(e) => {
panic!("Failed to read {}: {}", path_clone.display(), e);
}
}
}
}
}
}
record_map.into_values().collect::<Vec<_>>()
}
fn discover_groups<P: AsRef<Path>>(input_dir: P) -> HashSet<String> {
let mut groups = HashSet::new();
if let Ok(entries) = fs::read_dir(input_dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.ends_with(JSONL_EXTENSION) {
if let Some(underscore_pos) = file_name.find('_') {
let group_name = &file_name[..underscore_pos];
groups.insert(group_name.to_string());
}
}
}
}
}
groups
}
}
fn read_stored_crate_name(input_dir: &Path) -> Option<String> {
let crate_name_path = input_dir.join(CRATE_NAME_FILE);
fs::read_to_string(crate_name_path)
.ok()
.map(|s| s.trim().to_string())
}
fn read_features_from_out_dir(input_dir: &Path) -> Vec<String> {
let features_path = input_dir.join(FEATURES_FILE);
let Some(contents) = fs::read_to_string(features_path).ok() else {
return Vec::new();
};
let mut features: Vec<String> = contents
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(|s| s.to_string())
.collect();
features.sort();
features.dedup();
features
}
pub struct Builder {
input_dir: PathBuf,
features_constant: Option<String>,
target_triple: Option<String>,
crate_name: Option<String>,
}
impl Builder {
fn new<P: AsRef<Path>>(input_dir: P) -> Self {
let target = std::env::var("TARGET").unwrap_or_default();
let host = std::env::var("HOST").unwrap_or_default();
let target_triple = if !target.is_empty() {
Some(target)
} else if !host.is_empty() {
Some(host)
} else {
None
};
Self {
input_dir: input_dir.as_ref().to_path_buf(),
features_constant: Some("FEATURES".to_string()),
target_triple,
crate_name: None,
}
}
pub fn crate_name(mut self, name: impl Into<String>) -> Self {
self.crate_name = Some(name.into());
self
}
#[roxygen]
pub fn enable_feature_filtering(
mut self,
name: Option<impl Into<String>>,
) -> Self {
self.features_constant = name.map(|n| n.into());
self
}
#[roxygen]
pub fn enable_target_filtering(
mut self,
name: Option<impl Into<String>>,
) -> Self {
self.target_triple = name.map(|n| n.into());
self
}
pub fn build(self) -> Source {
Source::build_internal(
&self.input_dir,
self.features_constant,
self.target_triple,
self.crate_name,
)
}
}