• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# Copyright (c) 2018 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script that triggers and waits for tasks on android-compile.skia.org"""
7
8import base64
9import hashlib
10import json
11import math
12import optparse
13import os
14import requests
15import subprocess
16import sys
17import time
18
19INFRA_BOTS_DIR = os.path.abspath(os.path.realpath(os.path.join(
20    os.path.dirname(os.path.abspath(__file__)), os.pardir)))
21sys.path.insert(0, INFRA_BOTS_DIR)
22import utils
23
24
25ANDROID_COMPILE_BUCKET = 'android-compile-tasks'
26
27GS_RETRIES = 5
28GS_RETRY_WAIT_BASE = 15
29
30POLLING_FREQUENCY_SECS = 10
31DEADLINE_SECS = 2* 60 * 60  # 2 hours.
32
33INFRA_FAILURE_ERROR_MSG = (
34      '\n\n'
35      'Your run failed due to unknown infrastructure failures.\n'
36      'Please contact rmistry@ or the trooper from '
37      'http://skia-tree-status.appspot.com/trooper\n'
38      'Sorry for the inconvenience!\n'
39)
40
41
42class AndroidCompileException(Exception):
43  pass
44
45
46def _create_task_dict(options):
47  """Creates a dict representation of the requested task."""
48  params = {}
49  params['lunch_target'] = options.lunch_target
50  params['mmma_targets'] = options.mmma_targets
51  params['issue'] = options.issue
52  params['patchset'] = options.patchset
53  params['hash'] = options.hash
54  return params
55
56
57def _get_gs_bucket():
58  """Returns the Google storage bucket with the gs:// prefix."""
59  return 'gs://%s' % ANDROID_COMPILE_BUCKET
60
61
62def _write_to_storage(task):
63  """Writes the specified compile task to Google storage."""
64  with utils.tmp_dir():
65    json_file = os.path.join(os.getcwd(), _get_task_file_name(task))
66    with open(json_file, 'w') as f:
67      json.dump(task, f)
68    subprocess.check_call(['gsutil', 'cp', json_file, '%s/' % _get_gs_bucket()])
69    print 'Created %s/%s' % (_get_gs_bucket(), os.path.basename(json_file))
70
71
72def _get_task_file_name(task):
73  """Returns the file name of the compile task. Eg: ${issue}-${patchset}.json"""
74  return '%s-%s-%s.json' % (task['lunch_target'], task['issue'],
75                            task['patchset'])
76
77
78# Checks to see if task already exists in Google storage.
79# If the task has completed then the Google storage file is deleted.
80def _does_task_exist_in_storage(task):
81  """Checks to see if the corresponding file of the task exists in storage.
82
83  If the file exists and the task has already completed then the storage file is
84  deleted and False is returned.
85  """
86  gs_file = '%s/%s' % (_get_gs_bucket(), _get_task_file_name(task))
87  try:
88    output = subprocess.check_output(['gsutil', 'cat', gs_file])
89  except subprocess.CalledProcessError:
90    print 'Task does not exist in Google storage'
91    return False
92  taskJSON = json.loads(output)
93  if taskJSON.get('done'):
94    print 'Task exists in Google storage and has completed.'
95    print 'Deleting it so that a new run can be scheduled.'
96    subprocess.check_call(['gsutil', 'rm', gs_file])
97    return False
98  else:
99    print 'Tasks exists in Google storage and is still running.'
100    return True
101
102
103def _trigger_task(options):
104  """Triggers a task on the compile server by creating a file in storage."""
105  task = _create_task_dict(options)
106  # Check to see if file already exists in Google Storage.
107  if not _does_task_exist_in_storage(task):
108    _write_to_storage(task)
109  return task
110
111
112def trigger_and_wait(options):
113  """Triggers a task on the compile server and waits for it to complete."""
114  task = _trigger_task(options)
115  print 'Android Compile Task for %d/%d has been successfully added to %s.' % (
116      options.issue, options.patchset, ANDROID_COMPILE_BUCKET)
117  print '%s will be polled every %d seconds.' % (ANDROID_COMPILE_BUCKET,
118                                                 POLLING_FREQUENCY_SECS)
119
120  # Now poll the Google storage file till the task completes or till deadline
121  # is hit.
122  time_started_polling = time.time()
123  while True:
124    if (time.time() - time_started_polling) > DEADLINE_SECS:
125      raise AndroidCompileException(
126          'Task did not complete in the deadline of %s seconds.' % (
127              DEADLINE_SECS))
128
129    # Get the status of the task.
130    gs_file = '%s/%s' % (_get_gs_bucket(), _get_task_file_name(task))
131
132    for retry in range(GS_RETRIES):
133      try:
134        output = subprocess.check_output(['gsutil', 'cat', gs_file])
135      except subprocess.CalledProcessError:
136        raise AndroidCompileException('The %s file no longer exists.' % gs_file)
137      try:
138        ret = json.loads(output)
139        break
140      except ValueError, e:
141        print 'Received output that could not be converted to json: %s' % output
142        print e
143        if retry == (GS_RETRIES-1):
144          print '%d retries did not help' % GS_RETRIES
145          raise
146        waittime = GS_RETRY_WAIT_BASE * math.pow(2, retry)
147        print 'Retry in %d seconds.' % waittime
148        time.sleep(waittime)
149
150    if ret.get('infra_failure'):
151      if ret.get('error'):
152        raise AndroidCompileException('Run failed with:\n\n%s\n' % ret['error'])
153      else:
154        # Use a general purpose error message.
155        raise AndroidCompileException(INFRA_FAILURE_ERROR_MSG)
156
157    if ret.get('done'):
158      if not ret.get('is_master_branch', True):
159        print 'The Android Framework Compile bot only works for patches and'
160        print 'hashes from the master branch.'
161        return 0
162      elif ret['withpatch_success']:
163        print 'Your run was successfully completed.'
164        print 'With patch logs are here: %s' % ret['withpatch_log']
165        return 0
166      elif ret['nopatch_success']:
167        raise AndroidCompileException('The build with the patch failed and the '
168               'build without the patch succeeded. This means that the patch '
169               'causes Android to fail compilation.\n\n'
170               'With patch logs are here: %s\n\n'
171               'No patch logs are here: %s\n\n'
172               'You can force sync of the checkout if needed here: %s\n\n' % (
173                   ret['withpatch_log'], ret['nopatch_log'],
174                   'https://skia-android-compile.corp.goog/'))
175      else:
176        print ('Both with patch and no patch builds failed. This means that the'
177               ' Android tree is currently broken. Marking this bot as '
178               'successful')
179        print 'With patch logs are here: %s' % ret['withpatch_log']
180        print 'No patch logs are here: %s' % ret['nopatch_log']
181        return 0
182
183    # Print status of the task.
184    print 'Task: %s\n' % pretty_task_str(ret)
185    time.sleep(POLLING_FREQUENCY_SECS)
186
187
188def pretty_task_str(task):
189  status = 'Not picked up by server yet'
190  if task.get('task_id'):
191    status = 'Running withpatch compilation'
192  if task.get('withpatch_log'):
193    status = 'Running nopatch compilation'
194  return '[id: %s, checkout: %s, status: %s]' % (
195      task.get('task_id'), task.get('checkout'), status)
196
197
198def main():
199  option_parser = optparse.OptionParser()
200  option_parser.add_option(
201      '', '--lunch_target', type=str, default='',
202      help='The lunch target the android compile bot should build with.')
203  option_parser.add_option(
204      '', '--mmma_targets', type=str, default='',
205      help='The comma-separated mmma targets the android compile bot should '
206           'build.')
207  option_parser.add_option(
208      '', '--issue', type=int, default=0,
209      help='The Gerrit change number to get the patch from.')
210  option_parser.add_option(
211      '', '--patchset', type=int, default=0,
212      help='The Gerrit change patchset to use.')
213  option_parser.add_option(
214      '', '--hash', type=str, default='',
215      help='The Skia repo hash to compile against.')
216  options, _ = option_parser.parse_args()
217  sys.exit(trigger_and_wait(options))
218
219
220if __name__ == '__main__':
221  main()
222