use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::process::Command;
use syn::{Item, Visibility};
fn framework_prefixes(framework: &str) -> Option<&'static [&'static str]> {
match framework {
"CoreFoundation" => None,
"Foundation" => Some(&["NS", "__NS"]),
"AppKit" => Some(&[]),
"CoreData" => Some(&[]),
"CoreGraphics" => Some(&["CG", "__CG"]),
"CoreText" => Some(&["CT", "__CT"]),
"QuartzCore" => Some(&["CA", "__CA"]),
"CoreServices" => Some(&["LS", "UT", "MDItem", "FSEvent", "AE"]),
"CoreImage" => Some(&["CI", "__CI"]),
"CoreMedia" => Some(&["CM", "__CM"]),
"CoreVideo" => Some(&["CV", "__CV"]),
"CoreAudio" => Some(&["Audio", "kAudio"]),
"AVFoundation" => Some(&["AV", "__AV"]),
"Metal" => Some(&["MTL", "__MTL"]),
"IOKit" => Some(&["IO", "io_", "kIO"]),
"Security" => Some(&["Sec", "CSSM", "kSec"]),
"SystemConfiguration" => Some(&["SC", "kSC"]),
"ImageIO" => Some(&["CGImage", "kCGImage"]),
"ColorSync" => Some(&["ColorSync"]),
"Cocoa" => Some(&[]), _ => None,
}
}
const BINDGEN_COMMON_SYMBOLS: &[&str] = &[
"__BindgenBitfieldUnit",
"__BindgenComplex",
"__BindgenFloat16",
"__IncompleteArrayField",
"id",
];
fn is_bindgen_common_symbol(symbol: &str) -> bool {
BINDGEN_COMMON_SYMBOLS.contains(&symbol)
}
fn is_framework_owned_symbol(symbol: &str, framework: &str) -> bool {
if is_bindgen_common_symbol(symbol) {
return false;
}
match framework_prefixes(framework) {
Some(prefixes) if prefixes.is_empty() => false,
Some(prefixes) => prefixes.iter().any(|p| symbol.starts_with(p)),
None => true, }
}
pub fn get_filterable_dep_symbols(
dep_symbols: &HashSet<String>,
dep_framework: &str,
) -> HashSet<String> {
dep_symbols
.iter()
.filter(|s| is_framework_owned_symbol(s, dep_framework))
.cloned()
.collect()
}
pub fn get_sdk_version() -> String {
Command::new("xcrun")
.args(["--show-sdk-version"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string())
}
#[derive(Debug, PartialEq)]
pub struct CacheKey {
pub sdk_version: String,
pub bindgen_version: String,
pub apple_bindgen_version: String,
}
impl CacheKey {
pub fn current() -> Self {
Self {
sdk_version: get_sdk_version(),
bindgen_version: "0.72".to_string(), apple_bindgen_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
pub fn cache_subdir(&self) -> String {
format!(
"MacOSX{}-bindgen{}-apple_bindgen{}",
self.sdk_version, self.bindgen_version, self.apple_bindgen_version
)
}
}
pub fn load_cached_symbols(
cache_dir: &PathBuf,
framework: &str,
current_key: &CacheKey,
) -> Option<HashSet<String>> {
load_cached_framework(cache_dir, framework, current_key).map(|(syms, _)| syms)
}
pub fn load_cached_framework(
cache_dir: &PathBuf,
framework: &str,
current_key: &CacheKey,
) -> Option<(HashSet<String>, Vec<String>)> {
let versioned_dir = cache_dir.join(current_key.cache_subdir());
let cache_file = versioned_dir.join(format!("{}.toml", framework));
let content = std::fs::read_to_string(&cache_file).ok()?;
let mut symbols = Vec::new();
let mut dependencies = Vec::new();
let mut section = "";
for line in content.lines() {
let line = line.trim();
if line.starts_with("symbols") {
section = "symbols";
} else if line.starts_with("dependencies") {
section = "dependencies";
} else if line == "]" {
section = "";
} else if !section.is_empty() {
let val = line.trim_matches(|c| c == '"' || c == ',' || c == ' ');
if !val.is_empty() {
match section {
"symbols" => symbols.push(val.to_string()),
"dependencies" => dependencies.push(val.to_string()),
_ => {}
}
}
}
}
Some((symbols.into_iter().collect(), dependencies))
}
pub fn save_cached_symbols(
cache_dir: &PathBuf,
framework: &str,
key: &CacheKey,
unique_symbols: &HashSet<String>,
dependencies: &[String],
) {
let versioned_dir = cache_dir.join(key.cache_subdir());
let _ = std::fs::create_dir_all(&versioned_dir);
let cache_file = versioned_dir.join(format!("{}.toml", framework));
let mut sorted_symbols: Vec<_> = unique_symbols.iter().collect();
sorted_symbols.sort();
let mut sorted_deps: Vec<_> = dependencies.to_vec();
sorted_deps.sort();
let mut content = String::new();
content.push_str("dependencies = [\n");
for dep in &sorted_deps {
content.push_str(&format!(" \"{}\",\n", dep));
}
content.push_str("]\n\n");
content.push_str("symbols = [\n");
for sym in sorted_symbols {
content.push_str(&format!(" \"{}\",\n", sym));
}
content.push_str("]\n");
if let Err(e) = std::fs::write(&cache_file, content) {
eprintln!(
"Warning: Failed to write cache file {}: {}",
cache_file.display(),
e
);
}
}
pub fn load_deps() -> HashMap<String, Vec<String>> {
let deps_content = include_str!("../../deps.toml");
let mut deps = HashMap::new();
for line in deps_content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((name, rest)) = line.split_once(" = ") {
let deps_str = rest.trim_matches(|c| c == '[' || c == ']');
let dep_list: Vec<String> = deps_str
.split(',')
.map(|s| s.trim().trim_matches('"').to_string())
.filter(|s| !s.is_empty())
.collect();
if !dep_list.is_empty() {
deps.insert(name.to_string(), dep_list);
}
}
}
deps
}
pub fn collect_all_deps(
framework: &str,
deps: &HashMap<String, Vec<String>>,
result: &mut HashSet<String>,
) {
if let Some(direct_deps) = deps.get(framework) {
for dep in direct_deps {
if result.insert(dep.clone()) {
collect_all_deps(dep, deps, result);
}
}
}
}
pub fn topological_sort(frameworks: &[&str], deps: &HashMap<String, Vec<String>>) -> Vec<String> {
let framework_set: HashSet<&str> = frameworks.iter().copied().collect();
let mut result = Vec::new();
let mut visited = HashSet::new();
let mut temp_mark = HashSet::new();
fn visit(
node: &str,
deps: &HashMap<String, Vec<String>>,
framework_set: &HashSet<&str>,
visited: &mut HashSet<String>,
temp_mark: &mut HashSet<String>,
result: &mut Vec<String>,
) {
if visited.contains(node) {
return;
}
if temp_mark.contains(node) {
return;
}
temp_mark.insert(node.to_string());
if let Some(node_deps) = deps.get(node) {
for dep in node_deps {
if framework_set.contains(dep.as_str()) {
visit(dep, deps, framework_set, visited, temp_mark, result);
}
}
}
temp_mark.remove(node);
visited.insert(node.to_string());
result.push(node.to_string());
}
for &framework in frameworks {
visit(
framework,
deps,
&framework_set,
&mut visited,
&mut temp_mark,
&mut result,
);
}
result
}
pub fn extract_symbols(code: &str) -> HashSet<String> {
let mut symbols = HashSet::new();
let file = match syn::parse_file(code) {
Ok(f) => f,
Err(e) => {
eprintln!("Warning: Failed to parse generated code: {}", e);
return symbols;
}
};
for item in file.items {
if let Some(name) = extract_item_name(&item) {
if !name.starts_with("_bindgen_ty_") {
symbols.insert(name);
}
}
}
symbols
}
fn extract_item_name(item: &Item) -> Option<String> {
match item {
Item::Struct(s) if matches!(s.vis, Visibility::Public(_)) => Some(s.ident.to_string()),
Item::Enum(e) if matches!(e.vis, Visibility::Public(_)) => Some(e.ident.to_string()),
Item::Type(t) if matches!(t.vis, Visibility::Public(_)) => Some(t.ident.to_string()),
Item::Fn(f) if matches!(f.vis, Visibility::Public(_)) => Some(f.sig.ident.to_string()),
Item::Const(c) if matches!(c.vis, Visibility::Public(_)) => Some(c.ident.to_string()),
Item::Static(s) if matches!(s.vis, Visibility::Public(_)) => Some(s.ident.to_string()),
Item::Trait(t) if matches!(t.vis, Visibility::Public(_)) => Some(t.ident.to_string()),
Item::Union(u) if matches!(u.vis, Visibility::Public(_)) => Some(u.ident.to_string()),
_ => None,
}
}
fn get_impl_type_name(impl_item: &syn::ItemImpl) -> Option<String> {
match impl_item.self_ty.as_ref() {
syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
_ => None,
}
}
fn get_impl_trait_name(impl_item: &syn::ItemImpl) -> Option<String> {
impl_item
.trait_
.as_ref()
.and_then(|(_, path, _)| path.segments.last().map(|s| s.ident.to_string()))
}
fn use_references_dep_symbol(tree: &syn::UseTree, dep_symbols: &HashSet<String>) -> bool {
match tree {
syn::UseTree::Path(path) => use_references_dep_symbol(&path.tree, dep_symbols),
syn::UseTree::Name(name) => dep_symbols.contains(&name.ident.to_string()),
syn::UseTree::Rename(rename) => dep_symbols.contains(&rename.ident.to_string()),
syn::UseTree::Glob(_) => false,
syn::UseTree::Group(group) => group
.items
.iter()
.any(|t| use_references_dep_symbol(t, dep_symbols)),
}
}
fn use_references_unreachable(tree: &syn::UseTree, reachable: &HashSet<String>) -> bool {
match tree {
syn::UseTree::Path(path) => {
let segment = path.ident.to_string();
if segment == "self" {
return use_references_unreachable(&path.tree, reachable);
}
if matches!(
segment.as_str(),
"crate" | "super" | "std" | "core" | "alloc" | "objc" | "objc2"
) {
return false;
}
if !reachable.contains(&segment) {
return true;
}
use_references_unreachable(&path.tree, reachable)
}
syn::UseTree::Name(name) => {
let n = name.ident.to_string();
!reachable.contains(&n)
}
syn::UseTree::Rename(rename) => {
let n = rename.ident.to_string();
!reachable.contains(&n)
}
syn::UseTree::Glob(_) => false,
syn::UseTree::Group(group) => {
group
.items
.iter()
.all(|t| use_references_unreachable(t, reachable))
}
}
}
fn extract_use_rename(tree: &syn::UseTree) -> Option<(String, String)> {
match tree {
syn::UseTree::Path(path) if path.ident == "self" => extract_use_rename(&path.tree),
syn::UseTree::Rename(rename) => {
Some((rename.ident.to_string(), rename.rename.to_string()))
}
_ => None,
}
}
fn impl_dedup_key(impl_item: &syn::ItemImpl) -> String {
let type_name = get_impl_type_name(impl_item);
let trait_name = get_impl_trait_name(impl_item);
let discriminant = trait_name
.as_deref()
.map(|t| t.to_string())
.unwrap_or_else(|| {
impl_item
.items
.first()
.and_then(|it| match it {
syn::ImplItem::Fn(f) => Some(f.sig.ident.to_string()),
syn::ImplItem::Const(c) => Some(c.ident.to_string()),
syn::ImplItem::Type(t) => Some(t.ident.to_string()),
_ => None,
})
.unwrap_or_default()
});
format!(
"impl_{}_{}",
type_name.as_deref().unwrap_or(""),
discriminant
)
}
pub fn filter_to_reachable(
code: &str,
reachable: &HashSet<String>,
dep_frameworks: &[String],
available: Option<&HashSet<String>>,
) -> String {
let file = match syn::parse_file(code) {
Ok(f) => f,
Err(_) => return code.to_string(),
};
let mut filtered_items = Vec::new();
let mut extern_blocks = Vec::new();
let mut emitted_symbols: HashSet<String> = HashSet::new();
for item in file.items {
match &item {
Item::ForeignMod(fm) => {
let mut filtered_foreign_items = Vec::new();
for foreign_item in &fm.items {
let name = match foreign_item {
syn::ForeignItem::Fn(f) => Some(f.sig.ident.to_string()),
syn::ForeignItem::Static(s) => Some(s.ident.to_string()),
syn::ForeignItem::Type(t) => Some(t.ident.to_string()),
_ => None,
};
if let Some(n) = name {
if reachable.contains(&n) && emitted_symbols.insert(n) {
filtered_foreign_items.push(foreign_item.clone());
}
} else {
filtered_foreign_items.push(foreign_item.clone());
}
}
if !filtered_foreign_items.is_empty() {
let mut new_fm = fm.clone();
new_fm.items = filtered_foreign_items;
extern_blocks.push(Item::ForeignMod(new_fm));
}
}
Item::Impl(impl_item) => {
let type_name = get_impl_type_name(impl_item);
let type_reachable = type_name.as_ref().map_or(false, |n| reachable.contains(n));
if type_reachable {
let deps_satisfied = if let Some(avail) = available {
use super::depgraph::{impl_block_deps, is_builtin};
let deps = impl_block_deps(impl_item);
deps.iter().all(|dep| {
is_builtin(dep) || reachable.contains(dep) || avail.contains(dep)
})
} else {
true
};
if deps_satisfied {
if emitted_symbols.insert(impl_dedup_key(impl_item)) {
filtered_items.push(item);
}
}
}
}
Item::Use(use_item) => {
if !use_references_unreachable(&use_item.tree, reachable) {
filtered_items.push(item);
} else if let Some(avail) = available {
if let Some((source, alias)) = extract_use_rename(&use_item.tree) {
if reachable.contains(&alias) && avail.contains(&source) {
let type_alias: Item = syn::parse_str(&format!(
"pub type {alias} = {source};"
))
.expect("failed to parse type alias");
filtered_items.push(type_alias);
}
}
}
}
_ => {
if let Some(name) = extract_item_name(&item) {
if reachable.contains(&name) && emitted_symbols.insert(name) {
filtered_items.push(item);
}
} else {
filtered_items.push(item);
}
}
}
}
if let Ok(file2) = syn::parse_file(code) {
let surviving_names: HashSet<String> = filtered_items
.iter()
.filter_map(|item| extract_item_name(item))
.collect();
let available_set: HashSet<&str> = available
.iter()
.flat_map(|a| a.iter().map(|s| s.as_str()))
.collect();
let mut candidate_traits: HashSet<String> = HashSet::new();
for item in &file2.items {
if let Item::Impl(impl_item) = item {
let type_name = get_impl_type_name(impl_item);
let trait_name = get_impl_trait_name(impl_item);
if let (Some(tn), Some(trn)) = (&type_name, &trait_name) {
if surviving_names.contains(tn)
&& !surviving_names.contains(trn)
&& !available_set.contains(trn.as_str())
{
candidate_traits.insert(trn.clone());
}
}
}
}
use super::depgraph::is_builtin;
let mut needed_traits: HashSet<String> = HashSet::new();
for item in &file2.items {
if let Item::Trait(trait_item) = item {
let name = trait_item.ident.to_string();
if candidate_traits.contains(&name) {
let mut collector = super::depgraph::TypeRefCollector::new();
syn::visit::Visit::visit_item_trait(&mut collector, trait_item);
let deps_ok = collector.types.iter().all(|dep| {
dep == &name
|| is_builtin(dep)
|| surviving_names.contains(dep)
|| available_set.contains(dep.as_str())
});
if deps_ok {
needed_traits.insert(name);
}
}
}
}
for item in &file2.items {
if let Some(name) = extract_item_name(item) {
if needed_traits.contains(&name) && emitted_symbols.insert(name) {
filtered_items.push(item.clone());
}
}
}
for item in &file2.items {
if let Item::Impl(impl_item) = item {
let type_name = get_impl_type_name(impl_item);
let trait_name = get_impl_trait_name(impl_item);
let type_ok = type_name
.as_ref()
.map_or(false, |n| surviving_names.contains(n));
let trait_ok = trait_name.as_ref().map_or(true, |n| {
surviving_names.contains(n)
|| needed_traits.contains(n)
|| available_set.contains(n.as_str())
});
if type_ok && trait_ok {
if emitted_symbols.insert(impl_dedup_key(impl_item)) {
filtered_items.push(item.clone());
}
}
}
}
}
let mut output = String::new();
for dep in dep_frameworks {
output.push_str(&format!(
"#[allow(unused_imports)]\nuse crate::{}::*;\n",
dep
));
}
if !dep_frameworks.is_empty() {
output.push('\n');
}
use quote::ToTokens;
for item in filtered_items {
output.push_str(&item.to_token_stream().to_string());
output.push('\n');
}
for item in extern_blocks {
output.push_str(&item.to_token_stream().to_string());
output.push('\n');
}
output
}
pub fn filter_symbols(
code: &str,
dep_symbols: &HashSet<String>,
dep_frameworks: &[String],
) -> String {
let file = match syn::parse_file(code) {
Ok(f) => f,
Err(_) => return code.to_string(),
};
let mut filtered_items = Vec::new();
let mut extern_blocks = Vec::new();
let mut emitted_symbols: HashSet<String> = HashSet::new();
for item in file.items {
match &item {
Item::ForeignMod(fm) => {
let mut filtered_foreign_items = Vec::new();
for foreign_item in &fm.items {
let name = match foreign_item {
syn::ForeignItem::Fn(f) => Some(f.sig.ident.to_string()),
syn::ForeignItem::Static(s) => Some(s.ident.to_string()),
syn::ForeignItem::Type(t) => Some(t.ident.to_string()),
_ => None,
};
if let Some(n) = name {
if !dep_symbols.contains(&n) && emitted_symbols.insert(n) {
filtered_foreign_items.push(foreign_item.clone());
}
} else {
filtered_foreign_items.push(foreign_item.clone());
}
}
if !filtered_foreign_items.is_empty() {
let mut new_fm = fm.clone();
new_fm.items = filtered_foreign_items;
extern_blocks.push(Item::ForeignMod(new_fm));
}
}
Item::Impl(impl_item) => {
let type_name = get_impl_type_name(impl_item);
let trait_name = get_impl_trait_name(impl_item);
let type_in_deps = type_name
.as_ref()
.map_or(false, |n| dep_symbols.contains(n));
let trait_in_deps = trait_name
.as_ref()
.map_or(false, |n| dep_symbols.contains(n));
let impl_key = format!(
"impl_{}_{}",
type_name.as_deref().unwrap_or(""),
trait_name.as_deref().unwrap_or("")
);
if !type_in_deps && !trait_in_deps && emitted_symbols.insert(impl_key) {
filtered_items.push(item);
}
}
Item::Use(use_item) => {
if !use_references_dep_symbol(&use_item.tree, dep_symbols) {
filtered_items.push(item);
}
}
_ => {
if let Some(name) = extract_item_name(&item) {
if !dep_symbols.contains(&name) && emitted_symbols.insert(name) {
filtered_items.push(item);
}
} else {
filtered_items.push(item);
}
}
}
}
let mut output = String::new();
for dep in dep_frameworks {
output.push_str(&format!(
"#[allow(unused_imports)]\nuse crate::{}::*;\n",
dep
));
}
if !dep_frameworks.is_empty() {
output.push('\n');
}
use quote::ToTokens;
for item in filtered_items {
output.push_str(&item.to_token_stream().to_string());
output.push('\n');
}
for item in extern_blocks {
output.push_str(&item.to_token_stream().to_string());
output.push('\n');
}
output
}