• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Runs a tryjob/tryjobs after updating the packages."""
8
9from __future__ import print_function
10
11import argparse
12import datetime
13import json
14import os
15import subprocess
16
17import chroot
18import failure_modes
19import get_llvm_hash
20import update_chromeos_llvm_hash
21
22VALID_CQ_TRYBOTS = ['llvm', 'llvm-next', 'llvm-tot']
23
24
25def GetCommandLineArgs():
26  """Parses the command line for the command line arguments.
27
28  Returns:
29    The log level to use when retrieving the LLVM hash or google3 LLVM version,
30    the chroot path to use for executing chroot commands,
31    a list of a package or packages to update their LLVM next hash,
32    and the LLVM version to use when retrieving the LLVM hash.
33  """
34
35  # Default path to the chroot if a path is not specified.
36  cros_root = os.path.expanduser('~')
37  cros_root = os.path.join(cros_root, 'chromiumos')
38
39  # Create parser and add optional command-line arguments.
40  parser = argparse.ArgumentParser(
41      description='Update an LLVM hash of packages and run tests.')
42
43  # Add argument for other change lists that want to run alongside the tryjob
44  # which has a change list of updating a package's git hash.
45  parser.add_argument(
46      '--extra_change_lists',
47      type=int,
48      nargs='+',
49      default=[],
50      help='change lists that would like to be run alongside the change list '
51      'of updating the packages')
52
53  # Add argument for a specific chroot path.
54  parser.add_argument(
55      '--chroot_path',
56      default=cros_root,
57      help='the path to the chroot (default: %(default)s)')
58
59  # Add argument to choose between llvm and llvm-next.
60  parser.add_argument(
61      '--is_llvm_next',
62      action='store_true',
63      help='which llvm hash to update. Update LLVM_NEXT_HASH if specified. '
64      'Otherwise, update LLVM_HASH')
65
66  # Add argument for the absolute path to the file that contains information on
67  # the previous tested svn version.
68  parser.add_argument(
69      '--last_tested',
70      help='the absolute path to the file that contains the last tested '
71      'arguments.')
72
73  # Add argument for the LLVM version to use.
74  parser.add_argument(
75      '--llvm_version',
76      type=get_llvm_hash.is_svn_option,
77      required=True,
78      help='which git hash of LLVM to find '
79      '{google3, ToT, <svn_version>} '
80      '(default: finds the git hash of the google3 LLVM '
81      'version)')
82
83  # Add argument to add reviewers for the created CL.
84  parser.add_argument(
85      '--reviewers',
86      nargs='+',
87      default=[],
88      help='The reviewers for the package update changelist')
89
90  # Add argument for whether to display command contents to `stdout`.
91  parser.add_argument(
92      '--verbose',
93      action='store_true',
94      help='display contents of a command to the terminal '
95      '(default: %(default)s)')
96
97  subparsers = parser.add_subparsers(dest='subparser_name')
98  subparser_names = []
99  # Testing with the tryjobs.
100  tryjob_subparser = subparsers.add_parser('tryjobs')
101  subparser_names.append('tryjobs')
102  tryjob_subparser.add_argument(
103      '--builders',
104      required=True,
105      nargs='+',
106      default=[],
107      help='builders to use for the tryjob testing')
108
109  # Add argument for custom options for the tryjob.
110  tryjob_subparser.add_argument(
111      '--options',
112      required=False,
113      nargs='+',
114      default=[],
115      help='options to use for the tryjob testing')
116
117  # Testing with the recipe builders
118  recipe_subparser = subparsers.add_parser('recipe')
119  subparser_names.append('recipe')
120  recipe_subparser.add_argument(
121      '--options',
122      required=False,
123      nargs='+',
124      default=[],
125      help='options passed to the recipe builders')
126
127  recipe_subparser.add_argument(
128      '--builders',
129      required=True,
130      nargs='+',
131      default=[],
132      help='recipe builders to launch')
133
134  # Testing with CQ.
135  cq_subparser = subparsers.add_parser('cq')
136  subparser_names.append('cq')
137
138  # Add argument for specify a cq trybot to test along with other cq builders
139  # e.g. llvm, llvm-next or llvm-tot
140  cq_subparser.add_argument(
141      '--cq_trybot',
142      choices=VALID_CQ_TRYBOTS,
143      help='include the trybot to test together with other cq builders '
144      'available: %(choices)s')
145
146  args_output = parser.parse_args()
147
148  if args_output.subparser_name not in subparser_names:
149    parser.error('one of %s must be specified' % subparser_names)
150
151  return args_output
152
153
154def UnchangedSinceLastRun(last_tested_file, arg_dict):
155  """Gets the arguments used for last run
156
157  Args:
158    last_tested_file: The absolute path to the file that contains the
159    arguments for the last run.
160    arg_dict: The arguments used for this run.
161
162  Returns:
163    Return true if the arguments used for last run exist and are the
164    same as the arguments used for this run. Otherwise return false.
165  """
166
167  if not last_tested_file:
168    return False
169
170  # Get the last tested svn version if the file exists.
171  last_arg_dict = None
172  try:
173    with open(last_tested_file) as f:
174      last_arg_dict = json.load(f)
175
176  except (IOError, ValueError):
177    return False
178
179  return arg_dict == last_arg_dict
180
181
182def AddReviewers(cl, reviewers, chroot_path):
183  """Add reviewers for the created CL."""
184
185  gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
186  for reviewer in reviewers:
187    cmd = [gerrit_abs_path, 'reviewers', str(cl), reviewer]
188
189    subprocess.check_output(cmd)
190
191
192def AddLinksToCL(tests, cl, chroot_path):
193  """Adds the test link(s) to the CL as a comment."""
194
195  # NOTE: Invoking `cros_sdk` does not make each tryjob link appear on its own
196  # line, so invoking the `gerrit` command directly instead of using `cros_sdk`
197  # to do it for us.
198  #
199  # FIXME: Need to figure out why `cros_sdk` does not add each tryjob link as a
200  # newline.
201  gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
202
203  links = ['Started the following tests:']
204  links.extend(test['link'] for test in tests)
205
206  add_message_cmd = [gerrit_abs_path, 'message', str(cl), '\n'.join(links)]
207
208  subprocess.check_output(add_message_cmd)
209
210
211# Testing with tryjobs
212def GetCurrentTimeInUTC():
213  """Returns the current time via `datetime.datetime.utcnow()`."""
214  return datetime.datetime.utcnow()
215
216
217def GetTryJobCommand(change_list, extra_change_lists, options, builder):
218  """Constructs the 'tryjob' command.
219
220  Args:
221    change_list: The CL obtained from updating the packages.
222    extra_change_lists: Extra change lists that would like to be run alongside
223    the change list of updating the packages.
224    options: Options to be passed into the tryjob command.
225    builder: The builder to be passed into the tryjob command.
226
227  Returns:
228    The 'tryjob' command with the change list of updating the packages and
229    any extra information that was passed into the command line.
230  """
231
232  tryjob_cmd = ['cros', 'tryjob', '--yes', '--json', '-g', '%d' % change_list]
233
234  if extra_change_lists:
235    for extra_cl in extra_change_lists:
236      tryjob_cmd.extend(['-g', '%d' % extra_cl])
237
238  if options:
239    tryjob_cmd.extend('--%s' % option for option in options)
240
241  tryjob_cmd.append(builder)
242
243  return tryjob_cmd
244
245
246def RunTryJobs(cl_number, extra_change_lists, options, builders, chroot_path):
247  """Runs a tryjob/tryjobs.
248
249  Args:
250    cl_number: The CL created by updating the packages.
251    extra_change_lists: Any extra change lists that would run alongside the CL
252    that was created by updating the packages ('cl_number').
253    options: Any options to be passed into the 'tryjob' command.
254    builders: All the builders to run the 'tryjob' with.
255    chroot_path: The absolute path to the chroot.
256
257  Returns:
258    A list that contains stdout contents of each tryjob, where stdout is
259    information (a hashmap) about the tryjob. The hashmap also contains stderr
260    if there was an error when running a tryjob.
261
262  Raises:
263    ValueError: Failed to submit a tryjob.
264  """
265
266  # Contains the results of each builder.
267  tests = []
268
269  # Run tryjobs with the change list number obtained from updating the
270  # packages and append additional changes lists and options obtained from the
271  # command line.
272  for builder in builders:
273    cmd = GetTryJobCommand(cl_number, extra_change_lists, options, builder)
274
275    out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8')
276
277    test_output = json.loads(out)
278
279    tests.append({
280        'launch_time': str(GetCurrentTimeInUTC()),
281        'link': str(test_output[0]['url']),
282        'buildbucket_id': int(test_output[0]['buildbucket_id']),
283        'extra_cls': extra_change_lists,
284        'options': options,
285        'builder': [builder]
286    })
287
288  AddLinksToCL(tests, cl_number, chroot_path)
289
290  return tests
291
292
293def StartRecipeBuilders(cl_number, extra_change_lists, options, builders,
294                        chroot_path):
295  """Launch recipe builders.
296
297  Args:
298    cl_number: The CL created by updating the packages.
299    extra_change_lists: Any extra change lists that would run alongside the CL
300    that was created by updating the packages ('cl_number').
301    options: Any options to be passed into the 'tryjob' command.
302    builders: All the builders to run the 'tryjob' with.
303    chroot_path: The absolute path to the chroot.
304
305  Returns:
306    A list that contains stdout contents of each builder, where stdout is
307    information (a hashmap) about the tryjob. The hashmap also contains stderr
308    if there was an error when running a tryjob.
309
310  Raises:
311    ValueError: Failed to start a builder.
312  """
313
314  # Contains the results of each builder.
315  tests = []
316
317  # Launch a builders with the change list number obtained from updating the
318  # packages and append additional changes lists and options obtained from the
319  # command line.
320  for builder in builders:
321    cmd = ['bb', 'add', '-json']
322
323    if cl_number:
324      cmd.extend(['-cl', 'crrev.com/c/%d' % cl_number])
325
326    if extra_change_lists:
327      for cl in extra_change_lists:
328        cmd.extend(['-cl', 'crrev.com/c/%d' % cl])
329
330    if options:
331      cmd.extend(options)
332
333    cmd.append(builder)
334
335    out = subprocess.check_output(cmd, cwd=chroot_path, encoding='utf-8')
336
337    test_output = json.loads(out)
338
339    tests.append({
340        'launch_time': test_output['createTime'],
341        'link': 'http://ci.chromium.org/b/%s' % test_output['id'],
342        'buildbucket_id': test_output['id'],
343        'extra_cls': extra_change_lists,
344        'options': options,
345        'builder': [builder]
346    })
347
348  AddLinksToCL(tests, cl_number, chroot_path)
349
350  return tests
351
352
353# Testing with CQ
354def GetCQDependString(dependent_cls):
355  """Get CQ dependency string e.g. `Cq-Depend: chromium:MM, chromium:NN`."""
356
357  if not dependent_cls:
358    return None
359
360  # Cq-Depend must start a new paragraph prefixed with "Cq-Depend".
361  return '\nCq-Depend: ' + ', '.join(('chromium:%s' % i) for i in dependent_cls)
362
363
364def GetCQIncludeTrybotsString(trybot):
365  """Get Cq-Include-Trybots string, for more llvm testings"""
366
367  if not trybot:
368    return None
369
370  if trybot not in VALID_CQ_TRYBOTS:
371    raise ValueError('%s is not a valid llvm trybot' % trybot)
372
373  # Cq-Include-Trybots must start a new paragraph prefixed
374  # with "Cq-Include-Trybots".
375  return '\nCq-Include-Trybots:chromeos/cq:cq-%s-orchestrator' % trybot
376
377
378def StartCQDryRun(cl, dependent_cls, chroot_path):
379  """Start CQ dry run for the changelist and dependencies."""
380
381  gerrit_abs_path = os.path.join(chroot_path, 'chromite/bin/gerrit')
382
383  cl_list = [cl]
384  cl_list.extend(dependent_cls)
385
386  for changes in cl_list:
387    cq_dry_run_cmd = [gerrit_abs_path, 'label-cq', str(changes), '1']
388
389    subprocess.check_output(cq_dry_run_cmd)
390
391
392def main():
393  """Updates the packages' LLVM hash and run tests.
394
395  Raises:
396    AssertionError: The script was run inside the chroot.
397  """
398
399  chroot.VerifyOutsideChroot()
400
401  args_output = GetCommandLineArgs()
402
403  update_packages = [
404      'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
405      'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
406  ]
407
408  patch_metadata_file = 'PATCHES.json'
409
410  svn_option = args_output.llvm_version
411
412  git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(
413      svn_option)
414
415  # There is no need to run tryjobs when all the key parameters remain unchanged
416  # from last time.
417
418  # If --last_tested is specified, check if the current run has the same
419  # arguments last time --last_tested is used.
420  if args_output.last_tested:
421    chroot_file_paths = chroot.GetChrootEbuildPaths(args_output.chroot_path,
422                                                    update_packages)
423    arg_dict = {
424        'svn_version': svn_version,
425        'ebuilds': chroot_file_paths,
426        'extra_cls': args_output.extra_change_lists,
427    }
428    if args_output.subparser_name in ('tryjobs', 'recipe'):
429      arg_dict['builders'] = args_output.builders
430      arg_dict['tryjob_options'] = args_output.options
431    if UnchangedSinceLastRun(args_output.last_tested, arg_dict):
432      print('svn version (%d) matches the last tested svn version in %s' %
433            (svn_version, args_output.last_tested))
434      return
435
436  llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
437  if args_output.is_llvm_next:
438    llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
439  update_chromeos_llvm_hash.verbose = args_output.verbose
440  extra_commit_msg = None
441  if args_output.subparser_name == 'cq':
442    cq_depend_msg = GetCQDependString(args_output.extra_change_lists)
443    if cq_depend_msg:
444      extra_commit_msg = cq_depend_msg
445    cq_trybot_msg = GetCQIncludeTrybotsString(args_output.cq_trybot)
446    if cq_trybot_msg:
447      extra_commit_msg += cq_trybot_msg
448
449  change_list = update_chromeos_llvm_hash.UpdatePackages(
450      update_packages,
451      llvm_variant,
452      git_hash,
453      svn_version,
454      args_output.chroot_path,
455      patch_metadata_file,
456      failure_modes.FailureModes.DISABLE_PATCHES,
457      svn_option,
458      extra_commit_msg=extra_commit_msg)
459
460  AddReviewers(change_list.cl_number, args_output.reviewers,
461               args_output.chroot_path)
462
463  print('Successfully updated packages to %d' % svn_version)
464  print('Gerrit URL: %s' % change_list.url)
465  print('Change list number: %d' % change_list.cl_number)
466
467  if args_output.subparser_name == 'tryjobs':
468    tests = RunTryJobs(change_list.cl_number, args_output.extra_change_lists,
469                       args_output.options, args_output.builders,
470                       args_output.chroot_path)
471    print('Tests:')
472    for test in tests:
473      print(test)
474  elif args_output.subparser_name == 'recipe':
475    tests = StartRecipeBuilders(
476        change_list.cl_number, args_output.extra_change_lists,
477        args_output.options, args_output.builders, args_output.chroot_path)
478    print('Tests:')
479    for test in tests:
480      print(test)
481
482  else:
483    StartCQDryRun(change_list.cl_number, args_output.extra_change_lists,
484                  args_output.chroot_path)
485
486  # If --last_tested is specified, record the arguments used
487  if args_output.last_tested:
488    with open(args_output.last_tested, 'w') as f:
489      json.dump(arg_dict, f, indent=2)
490
491
492if __name__ == '__main__':
493  main()
494