Files
DroidDeck/tools/release/check_android_libraries.py

72 lines
3.7 KiB
Python

"""Validate the JNI entry points and Android ELF dependency closure in the actual APK."""
import argparse
from pathlib import Path
import re
import subprocess
import tempfile
from zipfile import ZipFile
SYSTEM = set('libc.so libm.so libdl.so liblog.so libandroid.so libz.so libvulkan.so libstdc++.so '
'libGLESv2.so libGLESv3.so libEGL.so libnativewindow.so libjnigraphics.so libaaudio.so '
'libOpenSLES.so libmediandk.so libcamera2ndk.so libsync.so libneuralnetworks.so'.split())
EXPORTS = ['Java_com_droiddeck_launcher_stores_StoresNative_nativeVersion',
'Java_com_droiddeck_launcher_stores_gog_GogNative_nativeStart',
'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeCall',
'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeInstall',
'Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeCall',
'Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeInstall']
def page_alignment(path, readelf, page_size):
headers = subprocess.check_output([readelf, '--program-headers', '--wide', str(path)], text=True)
loads = [line.split() for line in headers.splitlines() if line.strip().startswith('LOAD ')]
if not loads:
return ['Library has no loadable segments: ' + path.name]
return [f'{path.name} has a LOAD segment incompatible with {page_size}-byte pages'
for load in loads if int(load[-1], 16) < page_size or
(int(load[1], 16) - int(load[2], 16)) % page_size != 0]
def check(apk, readelf, stores=True, page_size=None):
errors = []
with tempfile.TemporaryDirectory() as temp, ZipFile(apk) as package:
libraries = {}
for name in package.namelist():
if (name.startswith('lib/arm64-v8a/') or name.startswith('base/lib/arm64-v8a/')) and name.endswith('.so'):
path = Path(temp) / Path(name).name
path.write_bytes(package.read(name))
libraries[path.name] = path
for name, path in libraries.items():
if page_size is not None:
errors.extend(page_alignment(path, readelf, page_size))
dynamic = subprocess.check_output([readelf, '-d', str(path)], text=True)
for dependency in re.findall(r'NEEDED.*\[(.*?)\]', dynamic):
if dependency not in libraries and dependency not in SYSTEM:
errors.append(name + ' needs missing ' + dependency)
library = libraries.get('libdroiddeckstores.so')
if stores:
if library is None:
errors.append('APK missing libdroiddeckstores.so')
else:
symbols = subprocess.check_output([readelf, '--dyn-syms', '--wide', str(library)], text=True)
exports = {line.split()[-1] for line in symbols.splitlines()
if ' GLOBAL ' in line and ' UND ' not in line and line.split()}
errors += ['Store library missing ' + symbol for symbol in EXPORTS if symbol not in exports]
elif library is not None:
errors.append('Rust was skipped but the APK contains a cached store library')
return errors
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('apk')
parser.add_argument('--readelf', default='readelf')
parser.add_argument('--without-stores', action='store_true')
parser.add_argument('--page-size', type=int, choices=[4096, 16384])
args = parser.parse_args()
errors = check(args.apk, args.readelf, not args.without_stores, args.page_size)
if errors:
parser.exit(1, '\n'.join(errors) + '\n')
print('APK native exports and dependencies verified')