1#! /usr/bin/env python 2# Copyright 2019 Google LLC. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6''' 7This script can be run with no arguments, in which case it will produce an 8APK with native libraries for all four architectures: arm, arm64, x86, and 9x64. You can instead list the architectures you want as arguments to this 10script. For example: 11 12 python create_apk.py arm x86 13 14The environment variables ANDROID_NDK and ANDROID_HOME must be set to the 15locations of the Android NDK and SDK. 16 17Additionally, `ninja` should be in your path. 18 19It assumes that the source tree is in the desired state, e.g. by having 20run 'python tools/git-sync-deps' in the root of the skia checkout. 21 22We also assume that the 'resources' directory has been copied to 23'platform_tools/android/apps/skqp/src/main/assets', and the 24'tools/skqp/download_model' script has been run. 25 26Also: 27 * If the environment variable SKQP_BUILD_DIR is set, many of the 28 intermediate build objects will be placed here. 29 * If the environment variable SKQP_OUTPUT_DIR is set, the final APK 30 will be placed in this directory. 31 * If the environment variable SKQP_DEBUG is set, Skia will be compiled 32 in debug mode. 33''' 34 35import os 36import re 37import subprocess 38import sys 39import shutil 40import time 41 42sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "../../../gn")) 43import skqp_gn_args 44 45def print_cmd(cmd, o): 46 m = re.compile('[^A-Za-z0-9_./-]') 47 o.write('+ ') 48 for c in cmd: 49 if m.search(c) is not None: 50 o.write(repr(c) + ' ') 51 else: 52 o.write(c + ' ') 53 o.write('\n') 54 o.flush() 55 56def check_call(cmd, **kwargs): 57 print_cmd(cmd, sys.stdout) 58 return subprocess.check_call(cmd, **kwargs) 59 60def find_name(searchpath, filename): 61 for dirpath, _, filenames in os.walk(searchpath): 62 if filename in filenames: 63 yield os.path.join(dirpath, filename) 64 65def check_ninja(): 66 with open(os.devnull, 'w') as devnull: 67 return subprocess.call(['ninja', '--version'], 68 stdout=devnull, stderr=devnull) == 0 69 70def remove(p): 71 if not os.path.islink(p) and os.path.isdir(p): 72 shutil.rmtree(p) 73 elif os.path.lexists(p): 74 os.remove(p) 75 assert not os.path.exists(p) 76 77def makedirs(dst): 78 if not os.path.exists(dst): 79 os.makedirs(dst) 80 81class RemoveFiles(object): 82 def __init__(self, *args): 83 self.args = args 84 def __enter__(self): 85 pass 86 def __exit__(self, a, b, c): 87 for arg in self.args: 88 remove(arg) 89 90class ChDir(object): 91 def __init__(self, d): 92 self.orig = os.getcwd() 93 os.chdir(d) 94 def __enter__(self): 95 pass 96 def __exit__(self, a, b, c): 97 os.chdir(self.orig) 98 99def make_symlinked_subdir(target, working_dir): 100 newdir = os.path.join(working_dir, os.path.basename(target)) 101 makedirs(newdir) 102 os.symlink(os.path.relpath(newdir, os.path.dirname(target)), target) 103 104def accept_android_license(android_home): 105 proc = subprocess.Popen( 106 [android_home + '/tools/bin/sdkmanager', '--licenses'], 107 stdin=subprocess.PIPE) 108 while proc.poll() is None: 109 proc.stdin.write('y\n') 110 time.sleep(1) 111 112# pylint: disable=bad-whitespace 113skia_to_android_arch_name_map = {'arm' : 'armeabi-v7a', 114 'arm64': 'arm64-v8a' , 115 'x86' : 'x86' , 116 'x64' : 'x86_64' } 117 118def create_apk_impl(opts): 119 build_dir, final_output_dir = opts.build_dir, opts.final_output_dir 120 121 assert os.path.exists('bin/gn') # Did you `tools/git-syc-deps`? 122 123 for d in [build_dir, final_output_dir]: 124 makedirs(d) 125 126 apps_dir = 'platform_tools/android/apps' 127 app = 'skqp' 128 lib = 'lib%s_app.so' % app 129 130 # These are the locations in the tree where the gradle needs or will create 131 # not-checked-in files. Treat them specially to keep the tree clean. 132 remove(build_dir + '/libs') 133 build_paths = [apps_dir + '/.gradle', 134 apps_dir + '/' + app + '/build', 135 apps_dir + '/' + app + '/src/main/libs'] 136 for path in build_paths: 137 remove(path) 138 try: 139 make_symlinked_subdir(path, build_dir) 140 except OSError: 141 sys.stderr.write('failed to create symlink "%s"\n' % path) 142 143 lib_dir = '%s/%s/src/main/libs' % (apps_dir, app) 144 apk_build_dir = '%s/%s/build/outputs/apk' % (apps_dir, app) 145 for d in [lib_dir, apk_build_dir]: 146 shutil.rmtree(d, True) # force rebuild 147 148 with RemoveFiles(*build_paths): 149 for arch in opts.architectures: 150 build = os.path.join(build_dir, arch) 151 gn_args = opts.gn_args(arch) 152 args = ' '.join('%s=%s' % (k, v) for k, v in gn_args.items()) 153 check_call(['bin/gn', 'gen', build, '--args=' + args]) 154 try: 155 check_call(['ninja', '-C', build, lib]) 156 except subprocess.CalledProcessError: 157 check_call(['ninja', '-C', build, '-t', 'clean']) 158 check_call(['ninja', '-C', build, lib]) 159 dst = '%s/%s' % (lib_dir, skia_to_android_arch_name_map[arch]) 160 makedirs(dst) 161 shutil.copy(os.path.join(build, lib), dst) 162 163 accept_android_license(opts.android_home) 164 env_copy = os.environ.copy() 165 env_copy['ANDROID_HOME'] = opts.android_home 166 env_copy['ANDROID_NDK_HOME'] = opts.android_ndk 167 # Why does gradlew need to be called from this directory? 168 check_call(['apps/gradlew', '-p' 'apps/' + app, 169 '-P', 'suppressNativeBuild', 170 ':%s:assembleUniversalDebug' % app], 171 env=env_copy, cwd='platform_tools/android') 172 173 apk_name = app + "-universal-debug.apk" 174 175 apk_list = list(find_name(apk_build_dir, apk_name)) 176 assert len(apk_list) == 1 177 178 out = os.path.join(final_output_dir, apk_name) 179 shutil.move(apk_list[0], out) 180 sys.stdout.write(out + '\n') 181 182 arches = '_'.join(sorted(opts.architectures)) 183 copy = os.path.join(final_output_dir, "%s-%s-debug.apk" % (app, arches)) 184 shutil.copyfile(out, copy) 185 sys.stdout.write(copy + '\n') 186 187 sys.stdout.write('* * * COMPLETE * * *\n\n') 188 189 190def create_apk(opts): 191 skia_dir = os.path.abspath(os.path.dirname(__file__) + '/../..') 192 assert os.path.exists(skia_dir) 193 with ChDir(skia_dir): 194 create_apk_impl(opts) 195 196class SkQP_Build_Options(object): 197 def __init__(self): 198 assert '/' in [os.sep, os.altsep] # 'a/b' over os.path.join('a', 'b') 199 self.error = '' 200 if not check_ninja(): 201 self.error += '`ninja` is not in the path.\n' 202 for var in ['ANDROID_NDK', 'ANDROID_HOME']: 203 if not os.path.exists(os.environ.get(var, '')): 204 self.error += 'Environment variable `%s` is not set.\n' % var 205 self.android_ndk = os.path.abspath(os.environ['ANDROID_NDK']) 206 self.android_home = os.path.abspath(os.environ['ANDROID_HOME']) 207 args = sys.argv[1:] 208 for arg in args: 209 if arg not in skia_to_android_arch_name_map: 210 self.error += ('Argument %r is not in %r\n' % 211 (arg, skia_to_android_arch_name_map.keys())) 212 self.architectures = args if args else skia_to_android_arch_name_map.keys() 213 default_build = os.path.dirname(__file__) + '/../../out/skqp' 214 self.build_dir = os.path.abspath(os.environ.get('SKQP_BUILD_DIR', default_build)) 215 self.final_output_dir = os.path.abspath(os.environ.get('SKQP_OUTPUT_DIR', default_build)) 216 self.debug = bool(os.environ.get('SKQP_DEBUG', '')) 217 218 def gn_args(self, arch): 219 return skqp_gn_args.GetGNArgs(arch=arch, ndk=self.android_ndk, debug=self.debug, 220 api_level=26) 221 222 def write(self, o): 223 for k, v in [('ANDROID_NDK', self.android_ndk), 224 ('ANDROID_HOME', self.android_home), 225 ('SKQP_OUTPUT_DIR', self.final_output_dir), 226 ('SKQP_BUILD_DIR', self.build_dir), 227 ('SKQP_DEBUG', self.debug), 228 ('Architectures', self.architectures)]: 229 o.write('%s = %r\n' % (k, v)) 230 o.flush() 231 232def main(): 233 options = SkQP_Build_Options() 234 if options.error: 235 sys.stderr.write(options.error + __doc__) 236 sys.exit(1) 237 options.write(sys.stdout) 238 create_apk(options) 239 240if __name__ == '__main__': 241 main() 242