#!/usr/bin/env python3 """校验 APK/AAB 里 64 位 .so 是否满足 Google Play 的 16KB 页对齐要求。 Google Play 自 2025-11-01 起要求 64 位原生库(arm64-v8a / x86_64)的每个 PT_LOAD 段 p_align >= 0x4000(16KB),不合规直接拒收,且**无延期**。 32 位(armeabi-v7a / x86)不受约束,脚本会列出但不判失败。 用法: python3 check_16kb.py build/app/outputs/bundle/release/app-release.aab python3 check_16kb.py build/app/outputs/flutter-apk/app-release.apk 退出码: 0=全部合规, 1=有不合规的 64 位库, 2=用法/文件错误 """ import struct import sys import zipfile REQUIRED_ALIGN = 0x4000 ABI64 = ("arm64-v8a", "x86_64") PT_LOAD = 1 def max_load_align(data: bytes): """返回 ELF 所有 PT_LOAD 段里最小的 p_align;不是 64 位 ELF 返回 None。""" if len(data) < 64 or data[:4] != b"\x7fELF": return None if data[4] != 2: # EI_CLASS: 2 = ELFCLASS64,32 位不参与判定 return None little = data[5] == 1 end = "<" if little else ">" # Elf64_Ehdr: e_phoff@0x20, e_phentsize@0x36, e_phnum@0x38 (e_phoff,) = struct.unpack_from(end + "Q", data, 0x20) (e_phentsize,) = struct.unpack_from(end + "H", data, 0x36) (e_phnum,) = struct.unpack_from(end + "H", data, 0x38) aligns = [] for i in range(e_phnum): off = e_phoff + i * e_phentsize if off + 56 > len(data): break (p_type,) = struct.unpack_from(end + "I", data, off) if p_type != PT_LOAD: continue # Elf64_Phdr: p_align 是最后一个字段,偏移 0x30 (p_align,) = struct.unpack_from(end + "Q", data, off + 0x30) aligns.append(p_align) return min(aligns) if aligns else None def abi_of(name: str): for abi in ("arm64-v8a", "armeabi-v7a", "x86_64", "x86"): if "/%s/" % abi in name: return abi return "?" def main(argv): if len(argv) != 2: print(__doc__) return 2 path = argv[1] try: zf = zipfile.ZipFile(path) except Exception as exc: # noqa: BLE001 print("无法打开 %s: %s" % (path, exc)) return 2 bad, ok, skipped = [], [], [] with zf: for info in zf.infolist(): if not info.filename.endswith(".so"): continue data = zf.read(info.filename) align = max_load_align(data) abi = abi_of(info.filename) if align is None: # 32 位或非 ELF:不参与 16KB 判定 skipped.append((info.filename, abi)) continue if align < REQUIRED_ALIGN: bad.append((info.filename, abi, align)) else: ok.append((info.filename, abi, align)) print("包: %s" % path) print("64 位 .so 合规 %d 个,不合规 %d 个;32 位/非 ELF 跳过 %d 个\n" % (len(ok), len(bad), len(skipped))) if bad: print("❌ 不合规(p_align < 0x4000,Play 会拒收):") for name, abi, align in sorted(bad): print(" %-60s %-12s p_align=0x%x" % (name, abi, align)) print("") print("修法:给产出该库的模块声明 NDK r28+(`ndkVersion`),或在 CMake 里加") print(" target_link_options( PRIVATE \"-Wl,-z,max-page-size=16384\")") print(" 改完记得清掉该模块的 android/.cxx 缓存,否则会复用旧产物。") return 1 print("✅ 全部 64 位 .so 均满足 16KB 页对齐") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))