3838import ctypes
3939import importlib
4040import json
41+ import re
4142import sys
4243import sysconfig
4344from io import StringIO
@@ -114,6 +115,9 @@ def _format_base_type_name(bt: object) -> str:
114115 if cls == "CComplexBaseTypeNode" :
115116 inner = _format_base_type_name (bt .base_type )
116117 return _unwrap_declarator (inner , bt .declarator )[0 ]
118+ if cls == "CConstOrVolatileTypeNode" :
119+ # Discard const/volatile qualifiers; they don't affect ABI layout
120+ return _format_base_type_name (bt .base_type )
117121 return cls
118122
119123
@@ -263,6 +267,62 @@ def get_structs(module: object) -> dict:
263267 return dict (sorted (structs .items ()))
264268
265269
270+ _OLD_ANON_RE = re .compile (r"^anon_(struct|union)\d+$" )
271+ _NEW_ANON_RE = re .compile (r"^\w+__anon_pod\d+$" )
272+
273+
274+ def _is_anon_name (name : str ) -> bool :
275+ return bool (_OLD_ANON_RE .match (name )) or bool (_NEW_ANON_RE .match (name ))
276+
277+
278+ def _normalize_type (type_str : str , rename_map : dict ) -> str :
279+ """Replace old anon_struct/union names in a type string using rename_map."""
280+ for old , new in rename_map .items ():
281+ # Use word-boundary matching so e.g. "anon_struct1" doesn't corrupt "anon_struct12"
282+ type_str = re .sub (r"\b" + re .escape (old ) + r"\b" , new , type_str )
283+ return type_str
284+
285+
286+ def _build_anon_rename_map (expected : dict , found : dict ) -> dict :
287+ """Match old anon_struct/union names to new MODULE__anon_pod names by field content.
288+
289+ Works iteratively bottom-up: leaf anon structs (whose fields contain no anon
290+ type references) are matched first. Their old->new mapping is then used to
291+ normalize the field types of the remaining unmatched anon structs, which may
292+ allow the next round to match parent structs that embed them. Repeats until
293+ no new matches are found in a round.
294+ """
295+ old_anons = {name : info for name , info in expected .items () if _is_anon_name (name )}
296+ new_pods = {name : info for name , info in found .items () if _is_anon_name (name )}
297+
298+ rename_map = {}
299+ matched_pods = set ()
300+ unmatched_old = dict (old_anons )
301+
302+ while True :
303+ matched_this_round = {}
304+
305+ for old_name , old_info in unmatched_old .items ():
306+ # Normalize this struct's field types using mappings found so far
307+ normalized_fields = [[_normalize_type (f [0 ], rename_map ), f [1 ]] for f in old_info .get ("fields" , [])]
308+ for new_name , new_info in new_pods .items ():
309+ if new_name in matched_pods :
310+ continue
311+ if normalized_fields == new_info .get ("fields" , []):
312+ matched_this_round [old_name ] = new_name
313+ matched_pods .add (new_name )
314+ break
315+
316+ if not matched_this_round :
317+ break # No progress; remaining old anons have no content-matching pod
318+
319+ rename_map .update (matched_this_round )
320+ for name in matched_this_round :
321+ del unmatched_old [name ]
322+
323+ return rename_map
324+
325+
266326def _report_field_changes (name : str , expected_fields : list , found_fields : list ) -> None :
267327 """Print detailed field-level differences for a struct."""
268328 expected_dict = {f [1 ]: f [0 ] for f in expected_fields }
@@ -289,12 +349,22 @@ def check_structs(expected: dict, found: dict) -> tuple[bool, bool]:
289349 has_errors = False
290350 has_allowed_changes = False
291351
352+ rename_map = _build_anon_rename_map (expected , found )
353+ renamed_new = set (rename_map .values ())
354+
292355 for name , expected_info in expected .items ():
293- if name not in found :
356+ effective_name = rename_map .get (name , name )
357+
358+ if effective_name not in found :
294359 print (f" Missing struct/class: { name } " )
295360 has_errors = True
296361 continue
297- found_info = found [name ]
362+
363+ if effective_name != name :
364+ # Anon struct/union renamed to new-style anon_pod — allowed change
365+ has_allowed_changes = True
366+
367+ found_info = found [effective_name ]
298368
299369 if "basicsize" in expected_info :
300370 if "basicsize" not in found_info :
@@ -310,12 +380,15 @@ def check_structs(expected: dict, found: dict) -> tuple[bool, bool]:
310380 if "fields" not in found_info :
311381 print (f" Struct { name } : field information no longer available" )
312382 has_errors = True
313- elif found_info ["fields" ] != expected_info ["fields" ]:
314- _report_field_changes (name , expected_info ["fields" ], found_info ["fields" ])
315- has_errors = True
383+ else :
384+ # Normalize old anon type names in expected field types before comparing
385+ normalized_fields = [[_normalize_type (f [0 ], rename_map ), f [1 ]] for f in expected_info ["fields" ]]
386+ if found_info ["fields" ] != normalized_fields :
387+ _report_field_changes (name , normalized_fields , found_info ["fields" ])
388+ has_errors = True
316389
317390 for name in found :
318- if name not in expected :
391+ if name not in expected and name not in renamed_new :
319392 print (f" Added struct/class: { name } " )
320393 has_allowed_changes = True
321394
@@ -426,6 +499,9 @@ def generate(package: str, abi_dir: Path) -> bool:
426499
427500 build_dir = get_package_path (package )
428501 for so_path in Path (build_dir ).glob (f"**/*{ EXT_SUFFIX } " ):
502+ if any (x .startswith ("_" ) for x in so_path .parts ):
503+ # Skip private modules (e.g. _driver.so) since they are not part of the public ABI
504+ continue
429505 try :
430506 module = import_from_path (package , build_dir , so_path )
431507 except ImportError :
0 commit comments