1use std::collections::{HashMap, HashSet};
7use std::path::PathBuf;
8use std::process::Command;
9use syn::{Item, Visibility};
10
11fn framework_prefixes(framework: &str) -> Option<&'static [&'static str]> {
14 match framework {
15 "CoreFoundation" => None,
17 "Foundation" => Some(&["NS", "__NS"]),
18 "AppKit" => Some(&[]),
20 "CoreData" => Some(&[]),
21 "CoreGraphics" => Some(&["CG", "__CG"]),
22 "CoreText" => Some(&["CT", "__CT"]),
23 "QuartzCore" => Some(&["CA", "__CA"]),
24 "CoreServices" => Some(&["LS", "UT", "MDItem", "FSEvent", "AE"]),
25 "CoreImage" => Some(&["CI", "__CI"]),
26 "CoreMedia" => Some(&["CM", "__CM"]),
27 "CoreVideo" => Some(&["CV", "__CV"]),
28 "CoreAudio" => Some(&["Audio", "kAudio"]),
29 "AVFoundation" => Some(&["AV", "__AV"]),
30 "Metal" => Some(&["MTL", "__MTL"]),
31 "IOKit" => Some(&["IO", "io_", "kIO"]),
32 "Security" => Some(&["Sec", "CSSM", "kSec"]),
33 "SystemConfiguration" => Some(&["SC", "kSC"]),
34 "ImageIO" => Some(&["CGImage", "kCGImage"]),
35 "ColorSync" => Some(&["ColorSync"]),
36 "Cocoa" => Some(&[]), _ => None,
38 }
39}
40
41const BINDGEN_COMMON_SYMBOLS: &[&str] = &[
43 "__BindgenBitfieldUnit",
44 "__BindgenComplex",
45 "__BindgenFloat16",
46 "__IncompleteArrayField",
47 "id",
48];
49
50fn is_bindgen_common_symbol(symbol: &str) -> bool {
52 BINDGEN_COMMON_SYMBOLS.contains(&symbol)
53}
54
55fn is_framework_owned_symbol(symbol: &str, framework: &str) -> bool {
57 if is_bindgen_common_symbol(symbol) {
60 return false;
61 }
62 match framework_prefixes(framework) {
63 Some(prefixes) if prefixes.is_empty() => false,
64 Some(prefixes) => prefixes.iter().any(|p| symbol.starts_with(p)),
65 None => true, }
67}
68
69pub fn get_filterable_dep_symbols(
71 dep_symbols: &HashSet<String>,
72 dep_framework: &str,
73) -> HashSet<String> {
74 dep_symbols
75 .iter()
76 .filter(|s| is_framework_owned_symbol(s, dep_framework))
77 .cloned()
78 .collect()
79}
80
81pub fn get_sdk_version() -> String {
83 Command::new("xcrun")
84 .args(["--show-sdk-version"])
85 .output()
86 .ok()
87 .and_then(|o| String::from_utf8(o.stdout).ok())
88 .map(|s| s.trim().to_string())
89 .unwrap_or_else(|| "unknown".to_string())
90}
91
92#[derive(Debug, PartialEq)]
94pub struct CacheKey {
95 pub sdk_version: String,
96 pub bindgen_version: String,
97 pub apple_bindgen_version: String,
98}
99
100impl CacheKey {
101 pub fn current() -> Self {
102 Self {
103 sdk_version: get_sdk_version(),
104 bindgen_version: "0.72".to_string(), apple_bindgen_version: env!("CARGO_PKG_VERSION").to_string(),
106 }
107 }
108
109 pub fn cache_subdir(&self) -> String {
111 format!(
112 "MacOSX{}-bindgen{}-apple_bindgen{}",
113 self.sdk_version, self.bindgen_version, self.apple_bindgen_version
114 )
115 }
116}
117
118pub fn load_cached_symbols(
120 cache_dir: &PathBuf,
121 framework: &str,
122 current_key: &CacheKey,
123) -> Option<HashSet<String>> {
124 load_cached_framework(cache_dir, framework, current_key).map(|(syms, _)| syms)
125}
126
127pub fn load_cached_framework(
129 cache_dir: &PathBuf,
130 framework: &str,
131 current_key: &CacheKey,
132) -> Option<(HashSet<String>, Vec<String>)> {
133 let versioned_dir = cache_dir.join(current_key.cache_subdir());
134 let cache_file = versioned_dir.join(format!("{}.toml", framework));
135 let content = std::fs::read_to_string(&cache_file).ok()?;
136
137 let mut symbols = Vec::new();
138 let mut dependencies = Vec::new();
139 let mut section = "";
140
141 for line in content.lines() {
142 let line = line.trim();
143 if line.starts_with("symbols") {
144 section = "symbols";
145 } else if line.starts_with("dependencies") {
146 section = "dependencies";
147 } else if line == "]" {
148 section = "";
149 } else if !section.is_empty() {
150 let val = line.trim_matches(|c| c == '"' || c == ',' || c == ' ');
151 if !val.is_empty() {
152 match section {
153 "symbols" => symbols.push(val.to_string()),
154 "dependencies" => dependencies.push(val.to_string()),
155 _ => {}
156 }
157 }
158 }
159 }
160
161 Some((symbols.into_iter().collect(), dependencies))
162}
163
164pub fn save_cached_symbols(
166 cache_dir: &PathBuf,
167 framework: &str,
168 key: &CacheKey,
169 unique_symbols: &HashSet<String>,
170 dependencies: &[String],
171) {
172 let versioned_dir = cache_dir.join(key.cache_subdir());
173 let _ = std::fs::create_dir_all(&versioned_dir);
174 let cache_file = versioned_dir.join(format!("{}.toml", framework));
175
176 let mut sorted_symbols: Vec<_> = unique_symbols.iter().collect();
177 sorted_symbols.sort();
178
179 let mut sorted_deps: Vec<_> = dependencies.to_vec();
180 sorted_deps.sort();
181
182 let mut content = String::new();
183 content.push_str("dependencies = [\n");
184 for dep in &sorted_deps {
185 content.push_str(&format!(" \"{}\",\n", dep));
186 }
187 content.push_str("]\n\n");
188 content.push_str("symbols = [\n");
189 for sym in sorted_symbols {
190 content.push_str(&format!(" \"{}\",\n", sym));
191 }
192 content.push_str("]\n");
193
194 if let Err(e) = std::fs::write(&cache_file, content) {
195 eprintln!(
196 "Warning: Failed to write cache file {}: {}",
197 cache_file.display(),
198 e
199 );
200 }
201}
202
203pub fn load_deps() -> HashMap<String, Vec<String>> {
205 let deps_content = include_str!("../../deps.toml");
206 let mut deps = HashMap::new();
207
208 for line in deps_content.lines() {
209 let line = line.trim();
210 if line.is_empty() || line.starts_with('#') {
211 continue;
212 }
213 if let Some((name, rest)) = line.split_once(" = ") {
214 let deps_str = rest.trim_matches(|c| c == '[' || c == ']');
215 let dep_list: Vec<String> = deps_str
216 .split(',')
217 .map(|s| s.trim().trim_matches('"').to_string())
218 .filter(|s| !s.is_empty())
219 .collect();
220 if !dep_list.is_empty() {
221 deps.insert(name.to_string(), dep_list);
222 }
223 }
224 }
225 deps
226}
227
228pub fn collect_all_deps(
230 framework: &str,
231 deps: &HashMap<String, Vec<String>>,
232 result: &mut HashSet<String>,
233) {
234 if let Some(direct_deps) = deps.get(framework) {
235 for dep in direct_deps {
236 if result.insert(dep.clone()) {
237 collect_all_deps(dep, deps, result);
238 }
239 }
240 }
241}
242
243pub fn topological_sort(frameworks: &[&str], deps: &HashMap<String, Vec<String>>) -> Vec<String> {
245 let framework_set: HashSet<&str> = frameworks.iter().copied().collect();
246 let mut result = Vec::new();
247 let mut visited = HashSet::new();
248 let mut temp_mark = HashSet::new();
249
250 fn visit(
251 node: &str,
252 deps: &HashMap<String, Vec<String>>,
253 framework_set: &HashSet<&str>,
254 visited: &mut HashSet<String>,
255 temp_mark: &mut HashSet<String>,
256 result: &mut Vec<String>,
257 ) {
258 if visited.contains(node) {
259 return;
260 }
261 if temp_mark.contains(node) {
262 return;
263 }
264 temp_mark.insert(node.to_string());
265
266 if let Some(node_deps) = deps.get(node) {
267 for dep in node_deps {
268 if framework_set.contains(dep.as_str()) {
269 visit(dep, deps, framework_set, visited, temp_mark, result);
270 }
271 }
272 }
273
274 temp_mark.remove(node);
275 visited.insert(node.to_string());
276 result.push(node.to_string());
277 }
278
279 for &framework in frameworks {
280 visit(
281 framework,
282 deps,
283 &framework_set,
284 &mut visited,
285 &mut temp_mark,
286 &mut result,
287 );
288 }
289
290 result
291}
292
293pub fn extract_symbols(code: &str) -> HashSet<String> {
295 let mut symbols = HashSet::new();
296
297 let file = match syn::parse_file(code) {
298 Ok(f) => f,
299 Err(e) => {
300 eprintln!("Warning: Failed to parse generated code: {}", e);
301 return symbols;
302 }
303 };
304
305 for item in file.items {
306 if let Some(name) = extract_item_name(&item) {
307 if !name.starts_with("_bindgen_ty_") {
311 symbols.insert(name);
312 }
313 }
314 }
315
316 symbols
317}
318
319fn extract_item_name(item: &Item) -> Option<String> {
321 match item {
322 Item::Struct(s) if matches!(s.vis, Visibility::Public(_)) => Some(s.ident.to_string()),
323 Item::Enum(e) if matches!(e.vis, Visibility::Public(_)) => Some(e.ident.to_string()),
324 Item::Type(t) if matches!(t.vis, Visibility::Public(_)) => Some(t.ident.to_string()),
325 Item::Fn(f) if matches!(f.vis, Visibility::Public(_)) => Some(f.sig.ident.to_string()),
326 Item::Const(c) if matches!(c.vis, Visibility::Public(_)) => Some(c.ident.to_string()),
327 Item::Static(s) if matches!(s.vis, Visibility::Public(_)) => Some(s.ident.to_string()),
328 Item::Trait(t) if matches!(t.vis, Visibility::Public(_)) => Some(t.ident.to_string()),
329 Item::Union(u) if matches!(u.vis, Visibility::Public(_)) => Some(u.ident.to_string()),
330 _ => None,
331 }
332}
333
334fn get_impl_type_name(impl_item: &syn::ItemImpl) -> Option<String> {
336 match impl_item.self_ty.as_ref() {
337 syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
338 _ => None,
339 }
340}
341
342fn get_impl_trait_name(impl_item: &syn::ItemImpl) -> Option<String> {
344 impl_item
345 .trait_
346 .as_ref()
347 .and_then(|(_, path, _)| path.segments.last().map(|s| s.ident.to_string()))
348}
349
350fn use_references_dep_symbol(tree: &syn::UseTree, dep_symbols: &HashSet<String>) -> bool {
352 match tree {
353 syn::UseTree::Path(path) => use_references_dep_symbol(&path.tree, dep_symbols),
354 syn::UseTree::Name(name) => dep_symbols.contains(&name.ident.to_string()),
355 syn::UseTree::Rename(rename) => dep_symbols.contains(&rename.ident.to_string()),
356 syn::UseTree::Glob(_) => false,
357 syn::UseTree::Group(group) => group
358 .items
359 .iter()
360 .any(|t| use_references_dep_symbol(t, dep_symbols)),
361 }
362}
363
364fn use_references_unreachable(tree: &syn::UseTree, reachable: &HashSet<String>) -> bool {
367 match tree {
368 syn::UseTree::Path(path) => {
369 let segment = path.ident.to_string();
370 if segment == "self" {
372 return use_references_unreachable(&path.tree, reachable);
373 }
374 if matches!(
376 segment.as_str(),
377 "crate" | "super" | "std" | "core" | "alloc" | "objc" | "objc2"
378 ) {
379 return false;
380 }
381 if !reachable.contains(&segment) {
383 return true;
384 }
385 use_references_unreachable(&path.tree, reachable)
386 }
387 syn::UseTree::Name(name) => {
388 let n = name.ident.to_string();
389 !reachable.contains(&n)
390 }
391 syn::UseTree::Rename(rename) => {
392 let n = rename.ident.to_string();
393 !reachable.contains(&n)
394 }
395 syn::UseTree::Glob(_) => false,
396 syn::UseTree::Group(group) => {
397 group
399 .items
400 .iter()
401 .all(|t| use_references_unreachable(t, reachable))
402 }
403 }
404}
405
406fn extract_use_rename(tree: &syn::UseTree) -> Option<(String, String)> {
408 match tree {
409 syn::UseTree::Path(path) if path.ident == "self" => extract_use_rename(&path.tree),
410 syn::UseTree::Rename(rename) => {
411 Some((rename.ident.to_string(), rename.rename.to_string()))
412 }
413 _ => None,
414 }
415}
416
417fn impl_dedup_key(impl_item: &syn::ItemImpl) -> String {
466 let type_name = get_impl_type_name(impl_item);
467 let trait_name = get_impl_trait_name(impl_item);
468 let discriminant = trait_name
469 .as_deref()
470 .map(|t| t.to_string())
471 .unwrap_or_else(|| {
472 impl_item
473 .items
474 .first()
475 .and_then(|it| match it {
476 syn::ImplItem::Fn(f) => Some(f.sig.ident.to_string()),
477 syn::ImplItem::Const(c) => Some(c.ident.to_string()),
478 syn::ImplItem::Type(t) => Some(t.ident.to_string()),
479 _ => None,
480 })
481 .unwrap_or_default()
482 });
483 format!(
484 "impl_{}_{}",
485 type_name.as_deref().unwrap_or(""),
486 discriminant
487 )
488}
489
490pub fn filter_to_reachable(
491 code: &str,
492 reachable: &HashSet<String>,
493 dep_frameworks: &[String],
494 available: Option<&HashSet<String>>,
495) -> String {
496 let file = match syn::parse_file(code) {
497 Ok(f) => f,
498 Err(_) => return code.to_string(),
499 };
500
501 let mut filtered_items = Vec::new();
502 let mut extern_blocks = Vec::new();
503 let mut emitted_symbols: HashSet<String> = HashSet::new();
504
505 for item in file.items {
506 match &item {
507 Item::ForeignMod(fm) => {
508 let mut filtered_foreign_items = Vec::new();
509 for foreign_item in &fm.items {
510 let name = match foreign_item {
511 syn::ForeignItem::Fn(f) => Some(f.sig.ident.to_string()),
512 syn::ForeignItem::Static(s) => Some(s.ident.to_string()),
513 syn::ForeignItem::Type(t) => Some(t.ident.to_string()),
514 _ => None,
515 };
516 if let Some(n) = name {
517 if reachable.contains(&n) && emitted_symbols.insert(n) {
518 filtered_foreign_items.push(foreign_item.clone());
519 }
520 } else {
521 filtered_foreign_items.push(foreign_item.clone());
522 }
523 }
524 if !filtered_foreign_items.is_empty() {
525 let mut new_fm = fm.clone();
526 new_fm.items = filtered_foreign_items;
527 extern_blocks.push(Item::ForeignMod(new_fm));
528 }
529 }
530 Item::Impl(impl_item) => {
531 let type_name = get_impl_type_name(impl_item);
532 let type_reachable = type_name.as_ref().map_or(false, |n| reachable.contains(n));
533
534 if type_reachable {
535 let deps_satisfied = if let Some(avail) = available {
538 use super::depgraph::{impl_block_deps, is_builtin};
539 let deps = impl_block_deps(impl_item);
540 deps.iter().all(|dep| {
541 is_builtin(dep) || reachable.contains(dep) || avail.contains(dep)
542 })
543 } else {
544 true
545 };
546
547 if deps_satisfied {
548 if emitted_symbols.insert(impl_dedup_key(impl_item)) {
549 filtered_items.push(item);
550 }
551 }
552 }
553 }
554 Item::Use(use_item) => {
555 if !use_references_unreachable(&use_item.tree, reachable) {
556 filtered_items.push(item);
557 } else if let Some(avail) = available {
558 if let Some((source, alias)) = extract_use_rename(&use_item.tree) {
562 if reachable.contains(&alias) && avail.contains(&source) {
563 let type_alias: Item = syn::parse_str(&format!(
564 "pub type {alias} = {source};"
565 ))
566 .expect("failed to parse type alias");
567 filtered_items.push(type_alias);
568 }
569 }
570 }
571 }
572 _ => {
573 if let Some(name) = extract_item_name(&item) {
574 if reachable.contains(&name) && emitted_symbols.insert(name) {
575 filtered_items.push(item);
576 }
577 } else {
578 filtered_items.push(item);
579 }
580 }
581 }
582 }
583
584 if let Ok(file2) = syn::parse_file(code) {
588 let surviving_names: HashSet<String> = filtered_items
589 .iter()
590 .filter_map(|item| extract_item_name(item))
591 .collect();
592
593 let available_set: HashSet<&str> = available
594 .iter()
595 .flat_map(|a| a.iter().map(|s| s.as_str()))
596 .collect();
597
598 let mut candidate_traits: HashSet<String> = HashSet::new();
601 for item in &file2.items {
602 if let Item::Impl(impl_item) = item {
603 let type_name = get_impl_type_name(impl_item);
604 let trait_name = get_impl_trait_name(impl_item);
605 if let (Some(tn), Some(trn)) = (&type_name, &trait_name) {
606 if surviving_names.contains(tn)
607 && !surviving_names.contains(trn)
608 && !available_set.contains(trn.as_str())
609 {
610 candidate_traits.insert(trn.clone());
611 }
612 }
613 }
614 }
615
616 use super::depgraph::is_builtin;
622 let mut needed_traits: HashSet<String> = HashSet::new();
623 for item in &file2.items {
624 if let Item::Trait(trait_item) = item {
625 let name = trait_item.ident.to_string();
626 if candidate_traits.contains(&name) {
627 let mut collector = super::depgraph::TypeRefCollector::new();
628 syn::visit::Visit::visit_item_trait(&mut collector, trait_item);
629 let deps_ok = collector.types.iter().all(|dep| {
630 dep == &name
631 || is_builtin(dep)
632 || surviving_names.contains(dep)
633 || available_set.contains(dep.as_str())
634 });
635 if deps_ok {
636 needed_traits.insert(name);
637 }
638 }
639 }
640 }
641
642 for item in &file2.items {
644 if let Some(name) = extract_item_name(item) {
645 if needed_traits.contains(&name) && emitted_symbols.insert(name) {
646 filtered_items.push(item.clone());
647 }
648 }
649 }
650
651 for item in &file2.items {
653 if let Item::Impl(impl_item) = item {
654 let type_name = get_impl_type_name(impl_item);
655 let trait_name = get_impl_trait_name(impl_item);
656
657 let type_ok = type_name
658 .as_ref()
659 .map_or(false, |n| surviving_names.contains(n));
660 let trait_ok = trait_name.as_ref().map_or(true, |n| {
661 surviving_names.contains(n)
662 || needed_traits.contains(n)
663 || available_set.contains(n.as_str())
664 });
665
666 if type_ok && trait_ok {
667 if emitted_symbols.insert(impl_dedup_key(impl_item)) {
668 filtered_items.push(item.clone());
669 }
670 }
671 }
672 }
673 }
674
675 let mut output = String::new();
676
677 for dep in dep_frameworks {
678 output.push_str(&format!(
679 "#[allow(unused_imports)]\nuse crate::{}::*;\n",
680 dep
681 ));
682 }
683 if !dep_frameworks.is_empty() {
684 output.push('\n');
685 }
686
687 use quote::ToTokens;
688 for item in filtered_items {
689 output.push_str(&item.to_token_stream().to_string());
690 output.push('\n');
691 }
692 for item in extern_blocks {
693 output.push_str(&item.to_token_stream().to_string());
694 output.push('\n');
695 }
696
697 output
698}
699
700pub fn filter_symbols(
702 code: &str,
703 dep_symbols: &HashSet<String>,
704 dep_frameworks: &[String],
705) -> String {
706 let file = match syn::parse_file(code) {
707 Ok(f) => f,
708 Err(_) => return code.to_string(),
709 };
710
711 let mut filtered_items = Vec::new();
712 let mut extern_blocks = Vec::new();
713 let mut emitted_symbols: HashSet<String> = HashSet::new();
715
716 for item in file.items {
717 match &item {
718 Item::ForeignMod(fm) => {
719 let mut filtered_foreign_items = Vec::new();
720 for foreign_item in &fm.items {
721 let name = match foreign_item {
722 syn::ForeignItem::Fn(f) => Some(f.sig.ident.to_string()),
723 syn::ForeignItem::Static(s) => Some(s.ident.to_string()),
724 syn::ForeignItem::Type(t) => Some(t.ident.to_string()),
725 _ => None,
726 };
727 if let Some(n) = name {
728 if !dep_symbols.contains(&n) && emitted_symbols.insert(n) {
729 filtered_foreign_items.push(foreign_item.clone());
730 }
731 } else {
732 filtered_foreign_items.push(foreign_item.clone());
733 }
734 }
735 if !filtered_foreign_items.is_empty() {
736 let mut new_fm = fm.clone();
737 new_fm.items = filtered_foreign_items;
738 extern_blocks.push(Item::ForeignMod(new_fm));
739 }
740 }
741 Item::Impl(impl_item) => {
742 let type_name = get_impl_type_name(impl_item);
743 let trait_name = get_impl_trait_name(impl_item);
744
745 let type_in_deps = type_name
746 .as_ref()
747 .map_or(false, |n| dep_symbols.contains(n));
748 let trait_in_deps = trait_name
749 .as_ref()
750 .map_or(false, |n| dep_symbols.contains(n));
751
752 let impl_key = format!(
754 "impl_{}_{}",
755 type_name.as_deref().unwrap_or(""),
756 trait_name.as_deref().unwrap_or("")
757 );
758
759 if !type_in_deps && !trait_in_deps && emitted_symbols.insert(impl_key) {
760 filtered_items.push(item);
761 }
762 }
763 Item::Use(use_item) => {
764 if !use_references_dep_symbol(&use_item.tree, dep_symbols) {
765 filtered_items.push(item);
766 }
767 }
768 _ => {
769 if let Some(name) = extract_item_name(&item) {
770 if !dep_symbols.contains(&name) && emitted_symbols.insert(name) {
771 filtered_items.push(item);
772 }
773 } else {
774 filtered_items.push(item);
775 }
776 }
777 }
778 }
779
780 let mut output = String::new();
781
782 for dep in dep_frameworks {
784 output.push_str(&format!(
785 "#[allow(unused_imports)]\nuse crate::{}::*;\n",
786 dep
787 ));
788 }
789 if !dep_frameworks.is_empty() {
790 output.push('\n');
791 }
792
793 use quote::ToTokens;
794 for item in filtered_items {
795 output.push_str(&item.to_token_stream().to_string());
796 output.push('\n');
797 }
798 for item in extern_blocks {
799 output.push_str(&item.to_token_stream().to_string());
800 output.push('\n');
801 }
802
803 output
804}