• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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