#! /usr/bin/env python # Copyright 2019 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 create_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. We also assume that the 'resources' directory has been copied to 'platform_tools/android/apps/skqp/src/main/assets', and the 'tools/skqp/download_model' script has been run. Also: * If the environment variable SKQP_BUILD_DIR is set, many of the intermediate build objects will be placed 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 re import subprocess import sys import shutil import time import skqp_gn_args 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 subprocess.call(['ninja', '--version'], stdout=devnull, stderr=devnull) == 0 def remove(p): if not os.path.islink(p) and os.path.isdir(p): shutil.rmtree(p) elif os.path.lexists(p): os.remove(p) assert not os.path.exists(p) def makedirs(dst): if not os.path.exists(dst): os.makedirs(dst) class RemoveFiles(object): def __init__(self, *args): self.args = args def __enter__(self): pass def __exit__(self, a, b, c): for arg in self.args: remove(arg) class ChDir(object): def __init__(self, d): self.orig = os.getcwd() os.chdir(d) def __enter__(self): pass def __exit__(self, a, b, c): os.chdir(self.orig) def make_symlinked_subdir(target, working_dir): newdir = os.path.join(working_dir, os.path.basename(target)) makedirs(newdir) os.symlink(os.path.relpath(newdir, os.path.dirname(target)), target) def accept_android_license(android_home): proc = subprocess.Popen( [android_home + '/tools/bin/sdkmanager', '--licenses'], stdin=subprocess.PIPE) while proc.poll() is None: proc.stdin.write('y\n') time.sleep(1) # pylint: disable=bad-whitespace skia_to_android_arch_name_map = {'arm' : 'armeabi-v7a', 'arm64': 'arm64-v8a' , 'x86' : 'x86' , 'x64' : 'x86_64' } def create_apk_impl(opts): build_dir, final_output_dir = opts.build_dir, opts.final_output_dir assert os.path.exists('bin/gn') # Did you `tools/git-syc-deps`? for d in [build_dir, final_output_dir]: makedirs(d) apps_dir = 'platform_tools/android/apps' app = 'skqp' lib = 'lib%s_app.so' % app # 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. remove(build_dir + '/libs') build_paths = [apps_dir + '/.gradle', apps_dir + '/' + app + '/build', apps_dir + '/' + app + '/src/main/libs'] for path in build_paths: remove(path) try: make_symlinked_subdir(path, build_dir) except OSError: sys.stderr.write('failed to create symlink "%s"\n' % path) lib_dir = '%s/%s/src/main/libs' % (apps_dir, app) apk_build_dir = '%s/%s/build/outputs/apk' % (apps_dir, app) for d in [lib_dir, apk_build_dir]: shutil.rmtree(d, True) # force rebuild with RemoveFiles(*build_paths): for arch in opts.architectures: build = os.path.join(build_dir, arch) gn_args = opts.gn_args(arch) args = ' '.join('%s=%s' % (k, v) for k, v in gn_args.items()) check_call(['bin/gn', 'gen', build, '--args=' + args]) try: check_call(['ninja', '-C', build, lib]) except subprocess.CalledProcessError: check_call(['ninja', '-C', build, '-t', 'clean']) check_call(['ninja', '-C', build, lib]) dst = '%s/%s' % (lib_dir, skia_to_android_arch_name_map[arch]) makedirs(dst) shutil.copy(os.path.join(build, lib), dst) accept_android_license(opts.android_home) env_copy = os.environ.copy() env_copy['ANDROID_HOME'] = opts.android_home env_copy['ANDROID_NDK_HOME'] = opts.android_ndk # Why does gradlew need to be called from this directory? check_call(['apps/gradlew', '-p' 'apps/' + app, '-P', 'suppressNativeBuild', ':%s:assembleUniversalDebug' % app], env=env_copy, cwd='platform_tools/android') 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') arches = '_'.join(sorted(opts.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 create_apk(opts): skia_dir = os.path.abspath(os.path.dirname(__file__) + '/../..') assert os.path.exists(skia_dir) with ChDir(skia_dir): create_apk_impl(opts) class SkQP_Build_Options(object): def __init__(self): assert '/' in [os.sep, os.altsep] # 'a/b' over os.path.join('a', 'b') self.error = '' if not check_ninja(): self.error += '`ninja` is not in the path.\n' for var in ['ANDROID_NDK', 'ANDROID_HOME']: if not os.path.exists(os.environ.get(var, '')): self.error += 'Environment variable `%s` is not set.\n' % var self.android_ndk = os.path.abspath(os.environ['ANDROID_NDK']) self.android_home = os.path.abspath(os.environ['ANDROID_HOME']) args = sys.argv[1:] for arg in args: if arg not in skia_to_android_arch_name_map: self.error += ('Argument %r is not in %r\n' % (arg, skia_to_android_arch_name_map.keys())) self.architectures = args if args else skia_to_android_arch_name_map.keys() default_build = os.path.dirname(__file__) + '/../../out/skqp' self.build_dir = os.path.abspath(os.environ.get('SKQP_BUILD_DIR', default_build)) self.final_output_dir = os.path.abspath(os.environ.get('SKQP_OUTPUT_DIR', default_build)) self.debug = bool(os.environ.get('SKQP_DEBUG', '')) def gn_args(self, arch): return skqp_gn_args.GetGNArgs(arch, self.android_ndk, self.debug, 26) def write(self, o): for k, v in [('ANDROID_NDK', self.android_ndk), ('ANDROID_HOME', self.android_home), ('SKQP_OUTPUT_DIR', self.final_output_dir), ('SKQP_BUILD_DIR', self.build_dir), ('SKQP_DEBUG', self.debug), ('Architectures', self.architectures)]: o.write('%s = %r\n' % (k, v)) o.flush() def main(): options = SkQP_Build_Options() if options.error: sys.stderr.write(options.error + __doc__) sys.exit(1) options.write(sys.stdout) create_apk(options) if __name__ == '__main__': main()