#! /usr/bin/env python # Copyright 2018 Google LLC. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. ''' This script can be run with no arguments, in which case it will produce an APK with native libraries for all four architectures: arm, arm64, x86, and x64. You can instead list the architectures you want as arguments to this script. For example: python make_universal_apk.py arm x86 The environment variables ANDROID_NDK and ANDROID_HOME must be set to the locations of the Android NDK and SDK. Additionally, `ninja` should be in your path. It assumes that the source tree is in the desired state, e.g. by having run 'python tools/git-sync-deps' in the root of the skia checkout. Also: * If the environment variable SKQP_BUILD_DIR is set, many of the intermediate build objects will be places here. * If the environment variable SKQP_OUTPUT_DIR is set, the final APK will be placed in this directory. * If the environment variable SKQP_DEBUG is set, Skia will be compiled in debug mode. ''' import os import glob import re import subprocess import sys import shutil def print_cmd(cmd, o): m = re.compile('[^A-Za-z0-9_./-]') o.write('+ ') for c in cmd: if m.search(c) is not None: o.write(repr(c) + ' ') else: o.write(c + ' ') o.write('\n') o.flush() def check_call(cmd, **kwargs): print_cmd(cmd, sys.stdout) return subprocess.check_call(cmd, **kwargs) def find_name(searchpath, filename): for dirpath, _, filenames in os.walk(searchpath): if filename in filenames: yield os.path.join(dirpath, filename) def check_ninja(): with open(os.devnull, 'w') as devnull: return 0 == subprocess.call(['ninja', '--version'], stdout=devnull, stderr=devnull) def remove(p): if not os.path.islink(p) and os.path.isdir(p): shutil.rmtree(p) elif os.path.exists(p): os.remove(p) assert not os.path.exists(p) skia_to_android_arch_name_map = {'arm' : 'armeabi-v7a', 'arm64': 'arm64-v8a' , 'x86' : 'x86' , 'x64' : 'x86_64' } def make_apk(architectures, android_ndk, android_home, build_dir, final_output_dir, debug, skia_dir): assert '/' in [os.sep, os.altsep] # 'a/b' over os.path.join('a', 'b') assert check_ninja() assert os.path.exists(android_ndk) assert os.path.exists(android_home) assert os.path.exists(skia_dir) assert architectures assert all(arch in skia_to_android_arch_name_map for arch in architectures) for d in [build_dir, final_output_dir]: if not os.path.exists(d): os.makedirs(d) os.chdir(skia_dir) apps_dir = 'platform_tools/android/apps' # These are the locations in the tree where the gradle needs or will create # not-checked-in files. Treat them specially to keep the tree clean. build_paths = [apps_dir + '/.gradle', apps_dir + '/skqp/build', apps_dir + '/skqp/src/main/libs', apps_dir + '/skqp/src/main/assets/resources', apps_dir + '/skqp/src/main/assets/gmkb'] remove(build_dir + '/libs') remove(build_dir + '/resources') for path in build_paths: remove(path) newdir = os.path.join(build_dir, os.path.basename(path)) if not os.path.exists(newdir): os.makedirs(newdir) try: os.symlink(os.path.relpath(newdir, os.path.dirname(path)), path) except OSError: pass app = 'skqp' lib = 'libskqp_app.so' shutil.rmtree(apps_dir + '/%s/src/main/libs' % app, True) if os.path.exists(apps_dir + '/skqp/src/main/assets/files.checksum'): check_call([sys.executable, 'tools/skqp/download_model']) else: sys.stderr.write( '\n* * *\n\nNote: SkQP models are missing!!!!\n\n* * *\n\n') check_call([sys.executable, 'tools/skqp/setup_resources']) for arch in architectures: build = os.path.join(build_dir, arch) gn_args = [android_ndk, '--arch', arch] if debug: build += '-debug' gn_args += ['--debug'] check_call([sys.executable, 'tools/skqp/generate_gn_args', build] + gn_args) check_call(['bin/gn', 'gen', build]) check_call(['ninja', '-C', build, lib]) dst = apps_dir + '/%s/src/main/libs/%s' % ( app, skia_to_android_arch_name_map[arch]) if not os.path.isdir(dst): os.makedirs(dst) shutil.copy(os.path.join(build, lib), dst) apk_build_dir = apps_dir + '/%s/build/outputs/apk' % app shutil.rmtree(apk_build_dir, True) # force rebuild # Why does gradlew need to be called from this directory? os.chdir('platform_tools/android') env_copy = os.environ.copy() env_copy['ANDROID_HOME'] = android_home check_call(['apps/gradlew', '-p' 'apps/' + app, '-P', 'suppressNativeBuild', ':%s:assembleUniversalDebug' % app], env=env_copy) os.chdir(skia_dir) apk_name = app + "-universal-debug.apk" apk_list = list(find_name(apk_build_dir, apk_name)) assert len(apk_list) == 1 out = os.path.join(final_output_dir, apk_name) shutil.move(apk_list[0], out) sys.stdout.write(out + '\n') for path in build_paths: remove(path) arches = '_'.join(sorted(architectures)) copy = os.path.join(final_output_dir, "%s-%s-debug.apk" % (app, arches)) shutil.copyfile(out, copy) sys.stdout.write(copy + '\n') sys.stdout.write('* * * COMPLETE * * *\n\n') def main(): def error(s): sys.stderr.write(s + __doc__) sys.exit(1) if not check_ninja(): error('`ninja` is not in the path.\n') for var in ['ANDROID_NDK', 'ANDROID_HOME']: if not os.path.exists(os.environ.get(var, '')): error('Environment variable `%s` is not set.\n' % var) architectures = sys.argv[1:] for arg in sys.argv[1:]: if arg not in skia_to_android_arch_name_map: error('Argument %r is not in %r\n' % (arg, skia_to_android_arch_name_map.keys())) if not architectures: architectures = skia_to_android_arch_name_map.keys() skia_dir = os.path.abspath( os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) default_build = os.path.join(skia_dir, 'out', 'skqp') build_dir = os.path.abspath(os.environ.get('SKQP_BUILD_DIR', default_build)) final_output_dir = os.path.abspath( os.environ.get('SKQP_OUTPUT_DIR', default_build)) debug = bool(os.environ.get('SKQP_DEBUG', '')) android_ndk = os.path.abspath(os.environ['ANDROID_NDK']) android_home = os.path.abspath(os.environ['ANDROID_HOME']) for k, v in [('ANDROID_NDK', android_ndk), ('ANDROID_HOME', android_home), ('skia root directory', skia_dir), ('SKQP_OUTPUT_DIR', final_output_dir), ('SKQP_BUILD_DIR', build_dir), ('Architectures', architectures)]: sys.stdout.write('%s = %r\n' % (k, v)) sys.stdout.flush() make_apk(architectures, android_ndk, android_home, build_dir, final_output_dir, debug, skia_dir) if __name__ == '__main__': main()