@@ -104,6 +104,85 @@ jobs:
104104 - name : List modules and their versions
105105 run : python3.13 -m pip freeze
106106
107+ - name : Verify universal2 extension modules
108+ shell : bash
109+ run : |
110+ python3.13 - <<'PY'
111+ import importlib.machinery
112+ import pathlib
113+ import fnmatch
114+ import site
115+ import subprocess
116+ import sys
117+
118+ ext_suffixes = tuple(importlib.machinery.EXTENSION_SUFFIXES)
119+ if not ext_suffixes:
120+ print("Could not determine extension suffixes.", file=sys.stderr)
121+ sys.exit(1)
122+
123+ allowlist_patterns = [
124+ # Add glob patterns for extension-module paths to exempt from universal2 checks.
125+ # Example: "*/site-packages/somepkg/someext*.so"
126+ ]
127+
128+ def iter_site_package_roots() -> list[pathlib.Path]:
129+ roots = set()
130+ for path in site.getsitepackages():
131+ roots.add(pathlib.Path(path).resolve())
132+ user_site = site.getusersitepackages()
133+ if user_site:
134+ roots.add(pathlib.Path(user_site).resolve())
135+ return sorted(roots)
136+
137+ def is_extension_module(path: pathlib.Path) -> bool:
138+ return any(path.name.endswith(suffix) for suffix in ext_suffixes)
139+
140+ def is_allowlisted(path: pathlib.Path) -> bool:
141+ normalized = path.as_posix()
142+ return any(fnmatch.fnmatch(normalized, pattern) for pattern in allowlist_patterns)
143+
144+ def check_arches(path: pathlib.Path) -> tuple[bool, str]:
145+ result = subprocess.run(
146+ ["lipo", "-archs", str(path)],
147+ check=False,
148+ capture_output=True,
149+ text=True,
150+ )
151+ if result.returncode != 0:
152+ return False, f"lipo failed: {result.stderr.strip()}"
153+ archs = set(result.stdout.strip().split())
154+ ok = {"arm64", "x86_64"}.issubset(archs)
155+ return ok, " ".join(sorted(archs))
156+
157+ failures = []
158+ checked = 0
159+
160+ for root in iter_site_package_roots():
161+ if not root.exists():
162+ continue
163+ for path in root.rglob("*"):
164+ if not path.is_file() or not is_extension_module(path):
165+ continue
166+ if is_allowlisted(path):
167+ continue
168+ checked += 1
169+ ok, details = check_arches(path)
170+ if not ok:
171+ failures.append((path, details))
172+
173+ print(f"Checked {checked} compiled extension modules for universal2 slices.")
174+
175+ if failures:
176+ print("Universal2 guard failed. Non-universal extension modules found:", file=sys.stderr)
177+ for path, details in failures[:50]:
178+ print(f" - {path}: {details}", file=sys.stderr)
179+ if len(failures) > 50:
180+ print(f" ... and {len(failures) - 50} more", file=sys.stderr)
181+ sys.exit(1)
182+
183+ print("Universal2 guard passed: all extension modules include arm64 and x86_64.")
184+ PY
185+
107186 - name : Build application
108187 id : build_app
109188 run : ./build.sh
0 commit comments