1#! /usr/bin/env python 2# Copyright 2018 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 make_universal_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 22Also: 23 * If the environment variable SKQP_BUILD_DIR is set, many of the 24 intermediate build objects will be places here. 25 * If the environment variable SKQP_OUTPUT_DIR is set, the final APK 26 will be placed in this directory. 27 * If the environment variable SKQP_DEBUG is set, Skia will be compiled 28 in debug mode. 29''' 30 31import os 32import glob 33import re 34import subprocess 35import sys 36import shutil 37 38def print_cmd(cmd, o): 39 m = re.compile('[^A-Za-z0-9_./-]') 40 o.write('+ ') 41 for c in cmd: 42 if m.search(c) is not None: 43 o.write(repr(c) + ' ') 44 else: 45 o.write(c + ' ') 46 o.write('\n') 47 o.flush() 48 49def check_call(cmd, **kwargs): 50 print_cmd(cmd, sys.stdout) 51 return subprocess.check_call(cmd, **kwargs) 52 53def find_name(searchpath, filename): 54 for dirpath, _, filenames in os.walk(searchpath): 55 if filename in filenames: 56 yield os.path.join(dirpath, filename) 57 58def check_ninja(): 59 with open(os.devnull, 'w') as devnull: 60 return 0 == subprocess.call(['ninja', '--version'], 61 stdout=devnull, stderr=devnull) 62 63def remove(p): 64 if not os.path.islink(p) and os.path.isdir(p): 65 shutil.rmtree(p) 66 elif os.path.lexists(p): 67 os.remove(p) 68 assert not os.path.exists(p) 69 70skia_to_android_arch_name_map = {'arm' : 'armeabi-v7a', 71 'arm64': 'arm64-v8a' , 72 'x86' : 'x86' , 73 'x64' : 'x86_64' } 74 75def make_apk(architectures, 76 android_ndk, 77 android_home, 78 build_dir, 79 final_output_dir, 80 debug, 81 skia_dir): 82 assert '/' in [os.sep, os.altsep] # 'a/b' over os.path.join('a', 'b') 83 assert check_ninja() 84 assert os.path.exists(android_ndk) 85 assert os.path.exists(android_home) 86 assert os.path.exists(skia_dir) 87 assert os.path.exists(skia_dir + '/bin/gn') # Did you `tools/git-syc-deps`? 88 assert architectures 89 assert all(arch in skia_to_android_arch_name_map 90 for arch in architectures) 91 92 for d in [build_dir, final_output_dir]: 93 if not os.path.exists(d): 94 os.makedirs(d) 95 96 os.chdir(skia_dir) 97 apps_dir = 'platform_tools/android/apps' 98 99 # These are the locations in the tree where the gradle needs or will create 100 # not-checked-in files. Treat them specially to keep the tree clean. 101 aosp_mode = os.path.exists('MODULE_LICENSE_BSD') 102 build_paths = [apps_dir + '/.gradle', 103 apps_dir + '/skqp/build', 104 apps_dir + '/skqp/src/main/libs'] 105 if not aosp_mode: 106 build_paths.append(apps_dir + '/skqp/src/main/assets/gmkb') 107 remove(build_dir + '/libs') 108 for path in build_paths: 109 remove(path) 110 newdir = os.path.join(build_dir, os.path.basename(path)) 111 if not os.path.exists(newdir): 112 os.makedirs(newdir) 113 try: 114 os.symlink(os.path.relpath(newdir, os.path.dirname(path)), path) 115 except OSError: 116 pass 117 118 if not aosp_mode: 119 resources_path = apps_dir + '/skqp/src/main/assets/resources' 120 remove(resources_path) 121 os.symlink('../../../../../../../resources', resources_path) 122 build_paths.append(resources_path) 123 124 app = 'skqp' 125 lib = 'libskqp_app.so' 126 127 shutil.rmtree(apps_dir + '/%s/src/main/libs' % app, True) 128 129 if not aosp_mode: 130 if os.path.exists(apps_dir + '/skqp/src/main/assets/files.checksum'): 131 check_call([sys.executable, 'tools/skqp/download_model']) 132 else: 133 sys.stderr.write( 134 '\n* * *\n\nNote: SkQP models are missing!!!!\n\n* * *\n\n') 135 if aosp_mode: 136 with open('include/config/SkUserConfig.h') as f: 137 user_config = f.readlines() 138 with open('include/config/SkUserConfig.h', 'w') as o: 139 for line in user_config: 140 m = re.match(r'^#define\s+([A-Za-z0-9_]+)(|\s.*)$', line) 141 if m: 142 o.write('#ifndef %s\n%s\n#endif\n' % (m.group(1), m.group(0).strip())) 143 else: 144 o.write(line) 145 146 for arch in architectures: 147 build = os.path.join(build_dir, arch) 148 gn_args = [android_ndk, '--arch', arch] 149 if debug: 150 build += '-debug' 151 gn_args += ['--debug'] 152 check_call([sys.executable, 'tools/skqp/generate_gn_args', build] 153 + gn_args) 154 check_call(['bin/gn', 'gen', build]) 155 check_call(['ninja', '-C', build, lib]) 156 dst = apps_dir + '/%s/src/main/libs/%s' % ( 157 app, skia_to_android_arch_name_map[arch]) 158 if not os.path.isdir(dst): 159 os.makedirs(dst) 160 shutil.copy(os.path.join(build, lib), dst) 161 162 if aosp_mode: 163 subprocess.call('git', 'checkout', 'HEAD', 'include/config/SkUserConfig.h') 164 165 apk_build_dir = apps_dir + '/%s/build/outputs/apk' % app 166 shutil.rmtree(apk_build_dir, True) # force rebuild 167 168 # Why does gradlew need to be called from this directory? 169 os.chdir('platform_tools/android') 170 env_copy = os.environ.copy() 171 env_copy['ANDROID_HOME'] = android_home 172 check_call(['apps/gradlew', '-p' 'apps/' + app, '-P', 'suppressNativeBuild', 173 ':%s:assembleUniversalDebug' % app], env=env_copy) 174 os.chdir(skia_dir) 175 176 apk_name = app + "-universal-debug.apk" 177 178 apk_list = list(find_name(apk_build_dir, apk_name)) 179 assert len(apk_list) == 1 180 181 out = os.path.join(final_output_dir, apk_name) 182 shutil.move(apk_list[0], out) 183 sys.stdout.write(out + '\n') 184 185 for path in build_paths: 186 remove(path) 187 188 arches = '_'.join(sorted(architectures)) 189 copy = os.path.join(final_output_dir, "%s-%s-debug.apk" % (app, arches)) 190 shutil.copyfile(out, copy) 191 sys.stdout.write(copy + '\n') 192 193 sys.stdout.write('* * * COMPLETE * * *\n\n') 194 195def main(): 196 def error(s): 197 sys.stderr.write(s + __doc__) 198 sys.exit(1) 199 if not check_ninja(): 200 error('`ninja` is not in the path.\n') 201 for var in ['ANDROID_NDK', 'ANDROID_HOME']: 202 if not os.path.exists(os.environ.get(var, '')): 203 error('Environment variable `%s` is not set.\n' % var) 204 architectures = sys.argv[1:] 205 for arg in sys.argv[1:]: 206 if arg not in skia_to_android_arch_name_map: 207 error('Argument %r is not in %r\n' % 208 (arg, skia_to_android_arch_name_map.keys())) 209 if not architectures: 210 architectures = skia_to_android_arch_name_map.keys() 211 skia_dir = os.path.abspath( 212 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) 213 default_build = os.path.join(skia_dir, 'out', 'skqp') 214 build_dir = os.path.abspath(os.environ.get('SKQP_BUILD_DIR', default_build)) 215 final_output_dir = os.path.abspath( 216 os.environ.get('SKQP_OUTPUT_DIR', default_build)) 217 debug = bool(os.environ.get('SKQP_DEBUG', '')) 218 android_ndk = os.path.abspath(os.environ['ANDROID_NDK']) 219 android_home = os.path.abspath(os.environ['ANDROID_HOME']) 220 221 for k, v in [('ANDROID_NDK', android_ndk), 222 ('ANDROID_HOME', android_home), 223 ('skia root directory', skia_dir), 224 ('SKQP_OUTPUT_DIR', final_output_dir), 225 ('SKQP_BUILD_DIR', build_dir), 226 ('Architectures', architectures)]: 227 sys.stdout.write('%s = %r\n' % (k, v)) 228 sys.stdout.flush() 229 make_apk(architectures, 230 android_ndk, 231 android_home, 232 build_dir, 233 final_output_dir, 234 debug, 235 skia_dir) 236 237if __name__ == '__main__': 238 main() 239 240