| #! /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_HOME 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 |
| |
| sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "../../../gn")) |
| 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_jni.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_HOME', '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_HOME']) |
| 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=arch, ndk=self.android_ndk, debug=self.debug, |
| api_level=26) |
| |
| def write(self, o): |
| for k, v in [('ANDROID_NDK_HOME', 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() |