You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

106 lines
3.5 KiB

#!/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(<target> 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))