1# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from recipe_engine import recipe_api
6
7import default_flavor
8import subprocess
9
10
11"""GN Android flavor utils, used for building Skia for Android with GN."""
12class GNAndroidFlavorUtils(default_flavor.DefaultFlavorUtils):
13  def __init__(self, m):
14    super(GNAndroidFlavorUtils, self).__init__(m)
15    self._ever_ran_adb = False
16
17    self.device_dirs = default_flavor.DeviceDirs(
18        dm_dir        = self.m.vars.android_data_dir + 'dm_out',
19        perf_data_dir = self.m.vars.android_data_dir + 'perf',
20        resource_dir  = self.m.vars.android_data_dir + 'resources',
21        images_dir    = self.m.vars.android_data_dir + 'images',
22        skp_dir       = self.m.vars.android_data_dir + 'skps',
23        svg_dir       = self.m.vars.android_data_dir + 'svgs',
24        tmp_dir       = self.m.vars.android_data_dir)
25
26  def _run(self, title, *cmd, **kwargs):
27    with self.m.step.context({'cwd': self.m.vars.skia_dir}):
28      return self.m.run(self.m.step, title, cmd=list(cmd), **kwargs)
29
30  def _py(self, title, script, infra_step=True):
31    with self.m.step.context({'cwd': self.m.vars.skia_dir}):
32      return self.m.run(self.m.python, title, script=script,
33                        infra_step=infra_step)
34
35  def _adb(self, title, *cmd, **kwargs):
36    self._ever_ran_adb = True
37    # The only non-infra adb steps (dm / nanobench) happen to not use _adb().
38    if 'infra_step' not in kwargs:
39      kwargs['infra_step'] = True
40    return self._run(title, 'adb', *cmd, **kwargs)
41
42  # Waits for an android device to be available
43  def _wait_for_device(self):
44    self.m.run(self.m.python.inline, 'wait for device', program="""
45      import subprocess
46      import sys
47      import time
48
49      kicks = 0
50      while True:
51
52        times = 0
53        while times < 30:
54          print 'Waiting for the device to be connected and ready.'
55          try:
56            times += 1
57            output = subprocess.check_output(['adb', 'shell',
58                                              'getprop', 'sys.boot_completed'])
59            if '1' in output:
60              print 'Connected'
61              sys.exit(0)
62          except subprocess.CalledProcessError:
63            # no device connected/authorized yet
64            pass
65          time.sleep(5)
66        if kicks >= 3:
67          break
68        print 'Giving the device a "kick" by trying to reboot it.'
69        kicks += 1
70        print subprocess.check_output(['adb', 'reboot'])
71
72      print 'Timed out waiting for device'
73      sys.exit(1)
74      """,
75      infra_step=True)
76
77
78  def compile(self, unused_target):
79    compiler      = self.m.vars.builder_cfg.get('compiler')
80    configuration = self.m.vars.builder_cfg.get('configuration')
81    extra_config  = self.m.vars.builder_cfg.get('extra_config', '')
82    os            = self.m.vars.builder_cfg.get('os')
83    target_arch   = self.m.vars.builder_cfg.get('target_arch')
84
85    assert compiler == 'Clang'  # At this rate we might not ever support GCC.
86
87    extra_cflags = []
88    if configuration == 'Debug':
89      extra_cflags.append('-O1')
90
91    ndk_asset = 'android_ndk_linux'
92    if 'Mac' in os:
93      ndk_asset = 'android_ndk_darwin'
94    elif 'Win' in os:
95      ndk_asset = 'n'
96
97    quote = lambda x: '"%s"' % x
98    args = {
99        'ndk': quote(self.m.vars.slave_dir.join(ndk_asset)),
100        'target_cpu': quote(target_arch),
101    }
102
103    if configuration != 'Debug':
104      args['is_debug'] = 'false'
105    if 'Vulkan' in extra_config:
106      args['ndk_api'] = 24
107      args['skia_enable_vulkan_debug_layers'] = 'false'
108    if 'FrameworkDefs' in extra_config:
109      args['skia_enable_android_framework_defines'] = 'true'
110    if extra_cflags:
111      args['extra_cflags'] = repr(extra_cflags).replace("'", '"')
112
113    gn_args = ' '.join('%s=%s' % (k,v) for (k,v) in sorted(args.iteritems()))
114
115    gn    = 'gn.exe'    if 'Win' in os else 'gn'
116    ninja = 'ninja.exe' if 'Win' in os else 'ninja'
117    gn = self.m.vars.skia_dir.join('bin', gn)
118
119    self._py('fetch-gn', self.m.vars.skia_dir.join('bin', 'fetch-gn'))
120    self._run('gn gen', gn, 'gen', self.out_dir, '--args=' + gn_args)
121    self._run('ninja', ninja, '-C', self.out_dir)
122
123  def install(self):
124    reboot_always = ['NexusPlayer', 'PixelC']
125    if self.m.vars.builder_cfg.get('model') in reboot_always:
126      self._adb('rebooting device', 'reboot')
127      self._wait_for_device()
128    self._adb('mkdir ' + self.device_dirs.resource_dir,
129              'shell', 'mkdir', '-p', self.device_dirs.resource_dir)
130
131
132  def cleanup_steps(self):
133    if self._ever_ran_adb:
134      self.m.run(self.m.python.inline, 'dump log', program="""
135      import os
136      import subprocess
137      import sys
138      out = sys.argv[1]
139      log = subprocess.check_output(['adb', 'logcat', '-d'])
140      for line in log.split('\\n'):
141        tokens = line.split()
142        if len(tokens) == 11 and tokens[-7] == 'F' and tokens[-3] == 'pc':
143          addr, path = tokens[-2:]
144          local = os.path.join(out, os.path.basename(path))
145          if os.path.exists(local):
146            sym = subprocess.check_output(['addr2line', '-Cfpe', local, addr])
147            line = line.replace(addr, addr + ' ' + sym.strip())
148        print line
149      """,
150      args=[self.m.vars.skia_out.join(self.m.vars.configuration)],
151      infra_step=True)
152
153    # Only shutdown the device and quarantine the bot if the first failed step
154    # is an infra step. If, instead, we did this for any infra failures, we
155    # would shutdown too much. For example, if a Nexus 10 died during dm
156    # and the following pull step would also fail "device not found" - causing
157    # us to run the shutdown command when the device was probably not in a
158    # broken state; it was just rebooting.
159    if (self.m.run.failed_steps and
160        isinstance(self.m.run.failed_steps[0], recipe_api.InfraFailure)):
161      self._adb('shut down device to quarantine bot', 'shell', 'reboot', '-p')
162
163    if self._ever_ran_adb:
164      self._adb('kill adb server', 'kill-server')
165
166  def step(self, name, cmd, **kwargs):
167    app = self.m.vars.skia_out.join(self.m.vars.configuration, cmd[0])
168    self._adb('push %s' % cmd[0],
169              'push', app, self.m.vars.android_bin_dir)
170
171    sh = '%s.sh' % cmd[0]
172    self.m.run.writefile(self.m.vars.tmp_dir.join(sh),
173        'set -x; %s%s; echo $? >%src' %
174        (self.m.vars.android_bin_dir, subprocess.list2cmdline(map(str, cmd)),
175            self.m.vars.android_bin_dir))
176    self._adb('push %s' % sh,
177              'push', self.m.vars.tmp_dir.join(sh), self.m.vars.android_bin_dir)
178
179    self._adb('clear log', 'logcat', '-c')
180    self.m.python.inline('%s' % cmd[0], """
181    import subprocess
182    import sys
183    bin_dir = sys.argv[1]
184    sh      = sys.argv[2]
185    subprocess.check_call(['adb', 'shell', 'sh', bin_dir + sh])
186    try:
187      sys.exit(int(subprocess.check_output(['adb', 'shell', 'cat',
188                                            bin_dir + 'rc'])))
189    except ValueError:
190      print "Couldn't read the return code.  Probably killed for OOM."
191      sys.exit(1)
192    """, args=[self.m.vars.android_bin_dir, sh])
193
194  def copy_file_to_device(self, host, device):
195    self._adb('push %s %s' % (host, device), 'push', host, device)
196
197  def copy_directory_contents_to_device(self, host, device):
198    # Copy the tree, avoiding hidden directories and resolving symlinks.
199    self.m.run(self.m.python.inline, 'push %s/* %s' % (host, device),
200               program="""
201    import os
202    import subprocess
203    import sys
204    host   = sys.argv[1]
205    device = sys.argv[2]
206    for d, _, fs in os.walk(host):
207      p = os.path.relpath(d, host)
208      if p != '.' and p.startswith('.'):
209        continue
210      for f in fs:
211        print os.path.join(p,f)
212        subprocess.check_call(['adb', 'push',
213                               os.path.realpath(os.path.join(host, p, f)),
214                               os.path.join(device, p, f)])
215    """, args=[host, device], infra_step=True)
216
217  def copy_directory_contents_to_host(self, device, host):
218    self._adb('pull %s %s' % (device, host), 'pull', device, host)
219
220  def read_file_on_device(self, path):
221    return self._adb('read %s' % path,
222                     'shell', 'cat', path, stdout=self.m.raw_io.output()).stdout
223
224  def remove_file_on_device(self, path):
225    self._adb('rm %s' % path, 'shell', 'rm', '-f', path)
226
227  def create_clean_device_dir(self, path):
228    self._adb('rm %s' % path, 'shell', 'rm', '-rf', path)
229    self._adb('mkdir %s' % path, 'shell', 'mkdir', '-p', path)
230