blob: 27bff792fbae3183f27b6a3f95ce7646da73a97a [file] [log] [blame]
Hal Canary82e3afa2019-08-19 15:04:24 -04001#! /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
42import skqp_gn_args
43
44def 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
55def check_call(cmd, **kwargs):
56 print_cmd(cmd, sys.stdout)
57 return subprocess.check_call(cmd, **kwargs)
58
59def 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
64def check_ninja():
65 with open(os.devnull, 'w') as devnull:
66 return subprocess.call(['ninja', '--version'],
67 stdout=devnull, stderr=devnull) == 0
68
69def 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
76def makedirs(dst):
77 if not os.path.exists(dst):
78 os.makedirs(dst)
79
80class 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
89class 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
98def 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
103def 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
112skia_to_android_arch_name_map = {'arm' : 'armeabi-v7a',
113 'arm64': 'arm64-v8a' ,
114 'x86' : 'x86' ,
115 'x64' : 'x86_64' }
116
117def 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 Canary2dad9902019-11-20 16:01:31 -0500153 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 Canary82e3afa2019-08-19 15:04:24 -0400158 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
189def 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
195class 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
230def 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
238if __name__ == '__main__':
239 main()