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