• 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"""Updates the LLVM hash and uprevs the build of the specified
8
9packages.
10
11For each package, a temporary repo is created and the changes are uploaded
12for review.
13"""
14
15from __future__ import print_function
16from datetime import datetime
17from enum import Enum
18
19import argparse
20import os
21import re
22import subprocess
23
24from failure_modes import FailureModes
25import chroot
26import get_llvm_hash
27import git
28import llvm_patch_management
29
30
31# Specify which LLVM hash to update
32class LLVMVariant(Enum):
33  """Represent the LLVM hash in an ebuild file to update."""
34
35  current = 'LLVM_HASH'
36  next = 'LLVM_NEXT_HASH'
37
38
39# If set to `True`, then the contents of `stdout` after executing a command will
40# be displayed to the terminal.
41verbose = False
42
43
44def GetCommandLineArgs():
45  """Parses the command line for the optional command line arguments.
46
47  Returns:
48    The log level to use when retrieving the LLVM hash or google3 LLVM version,
49    the chroot path to use for executing chroot commands,
50    a list of a package or packages to update their LLVM next hash,
51    and the LLVM version to use when retrieving the LLVM hash.
52  """
53
54  # Default path to the chroot if a path is not specified.
55  cros_root = os.path.expanduser('~')
56  cros_root = os.path.join(cros_root, 'chromiumos')
57
58  # Create parser and add optional command-line arguments.
59  parser = argparse.ArgumentParser(
60      description="Updates the build's hash for llvm-next.")
61
62  # Add argument for a specific chroot path.
63  parser.add_argument(
64      '--chroot_path',
65      default=cros_root,
66      help='the path to the chroot (default: %(default)s)')
67
68  # Add argument for specific builds to uprev and update their llvm-next hash.
69  parser.add_argument(
70      '--update_packages',
71      default=['sys-devel/llvm'],
72      required=False,
73      nargs='+',
74      help='the ebuilds to update their hash for llvm-next ' \
75          '(default: %(default)s)')
76
77  # Add argument for whether to display command contents to `stdout`.
78  parser.add_argument(
79      '--verbose',
80      action='store_true',
81      help='display contents of a command to the terminal '
82      '(default: %(default)s)')
83
84  # Add argument for the LLVM hash to update
85  parser.add_argument(
86      '--is_llvm_next',
87      action='store_true',
88      help='which llvm hash to update. If specified, update LLVM_NEXT_HASH. '
89      'Otherwise, update LLVM_HASH')
90
91  # Add argument for the LLVM version to use.
92  parser.add_argument(
93      '--llvm_version',
94      type=get_llvm_hash.is_svn_option,
95      required=True,
96      help='which git hash to use. Either a svn revision, or one '
97      'of %s' % sorted(get_llvm_hash.KNOWN_HASH_SOURCES))
98
99  # Add argument for the mode of the patch management when handling patches.
100  parser.add_argument(
101      '--failure_mode',
102      default=FailureModes.FAIL.value,
103      choices=[FailureModes.FAIL.value, FailureModes.CONTINUE.value,
104               FailureModes.DISABLE_PATCHES.value,
105               FailureModes.REMOVE_PATCHES.value],
106      help='the mode of the patch manager when handling failed patches ' \
107          '(default: %(default)s)')
108
109  # Add argument for the patch metadata file.
110  parser.add_argument(
111      '--patch_metadata_file',
112      default='PATCHES.json',
113      help='the .json file that has all the patches and their '
114      'metadata if applicable (default: PATCHES.json inside $FILESDIR)')
115
116  # Parse the command line.
117  args_output = parser.parse_args()
118
119  # FIXME: We shouldn't be using globals here, but until we fix it, make pylint
120  # stop complaining about it.
121  # pylint: disable=global-statement
122  global verbose
123
124  verbose = args_output.verbose
125
126  return args_output
127
128
129def GetEbuildPathsFromSymLinkPaths(symlinks):
130  """Reads the symlink(s) to get the ebuild path(s) to the package(s).
131
132  Args:
133    symlinks: A list of absolute path symlink/symlinks that point
134    to the package's ebuild.
135
136  Returns:
137    A dictionary where the key is the absolute path of the symlink and the value
138    is the absolute path to the ebuild that was read from the symlink.
139
140  Raises:
141    ValueError: Invalid symlink(s) were provided.
142  """
143
144  # A dictionary that holds:
145  #   key: absolute symlink path
146  #   value: absolute ebuild path
147  resolved_paths = {}
148
149  # Iterate through each symlink.
150  #
151  # For each symlink, check that it is a valid symlink,
152  # and then construct the ebuild path, and
153  # then add the ebuild path to the dict.
154  for cur_symlink in symlinks:
155    if not os.path.islink(cur_symlink):
156      raise ValueError('Invalid symlink provided: %s' % cur_symlink)
157
158    # Construct the absolute path to the ebuild.
159    ebuild_path = os.path.realpath(cur_symlink)
160
161    if cur_symlink not in resolved_paths:
162      resolved_paths[cur_symlink] = ebuild_path
163
164  return resolved_paths
165
166
167def UpdateEbuildLLVMHash(ebuild_path, llvm_variant, git_hash, svn_version):
168  """Updates the LLVM hash in the ebuild.
169
170  The build changes are staged for commit in the temporary repo.
171
172  Args:
173    ebuild_path: The absolute path to the ebuild.
174    llvm_variant: Which LLVM hash to update.
175    git_hash: The new git hash.
176    svn_version: The SVN-style revision number of git_hash.
177
178  Raises:
179    ValueError: Invalid ebuild path provided or failed to stage the commit
180    of the changes or failed to update the LLVM hash.
181  """
182
183  # Iterate through each ebuild.
184  #
185  # For each ebuild, read the file in
186  # advance and then create a temporary file
187  # that gets updated with the new LLVM hash
188  # and revision number and then the ebuild file
189  # gets updated to the temporary file.
190
191  if not os.path.isfile(ebuild_path):
192    raise ValueError('Invalid ebuild path provided: %s' % ebuild_path)
193
194  temp_ebuild_file = '%s.temp' % ebuild_path
195
196  with open(ebuild_path) as ebuild_file:
197    # write updates to a temporary file in case of interrupts
198    with open(temp_ebuild_file, 'w') as temp_file:
199      for cur_line in ReplaceLLVMHash(ebuild_file, llvm_variant, git_hash,
200                                      svn_version):
201        temp_file.write(cur_line)
202
203  os.rename(temp_ebuild_file, ebuild_path)
204
205  # Get the path to the parent directory.
206  parent_dir = os.path.dirname(ebuild_path)
207
208  # Stage the changes.
209  subprocess.check_output(['git', '-C', parent_dir, 'add', ebuild_path])
210
211
212def ReplaceLLVMHash(ebuild_lines, llvm_variant, git_hash, svn_version):
213  """Updates the LLVM git hash.
214
215  Args:
216    ebuild_lines: The contents of the ebuild file.
217    llvm_variant: The LLVM hash to update.
218    git_hash: The new git hash.
219    svn_version: The SVN-style revision number of git_hash.
220  """
221  is_updated = False
222  llvm_regex = re.compile('^' + re.escape(llvm_variant.value) +
223                          '=\"[a-z0-9]+\"')
224  for cur_line in ebuild_lines:
225    if not is_updated and llvm_regex.search(cur_line):
226      # Update the git hash and revision number.
227      cur_line = '%s=\"%s\" # r%d\n' % (llvm_variant.value, git_hash,
228                                        svn_version)
229
230      is_updated = True
231
232    yield cur_line
233
234  if not is_updated:
235    raise ValueError('Failed to update %s' % llvm_variant.value)
236
237
238def UprevEbuildSymlink(symlink):
239  """Uprevs the symlink's revision number.
240
241  Increases the revision number by 1 and stages the change in
242  the temporary repo.
243
244  Args:
245    symlink: The absolute path of an ebuild symlink.
246
247  Raises:
248    ValueError: Failed to uprev the symlink or failed to stage the changes.
249  """
250
251  if not os.path.islink(symlink):
252    raise ValueError('Invalid symlink provided: %s' % symlink)
253
254  new_symlink, is_changed = re.subn(
255      r'r([0-9]+).ebuild',
256      lambda match: 'r%s.ebuild' % str(int(match.group(1)) + 1),
257      symlink,
258      count=1)
259
260  if not is_changed:
261    raise ValueError('Failed to uprev the symlink.')
262
263  # rename the symlink
264  subprocess.check_output(
265      ['git', '-C',
266       os.path.dirname(symlink), 'mv', symlink, new_symlink])
267
268
269def UprevEbuildToVersion(symlink, svn_version):
270  """Uprevs the ebuild's revision number.
271
272  Increases the revision number by 1 and stages the change in
273  the temporary repo.
274
275  Args:
276    symlink: The absolute path of an ebuild symlink.
277    svn_version: The SVN-style revision number of git_hash.
278
279  Raises:
280    ValueError: Failed to uprev the ebuild or failed to stage the changes.
281  """
282
283  if not os.path.islink(symlink):
284    raise ValueError('Invalid symlink provided: %s' % symlink)
285
286  ebuild = os.path.realpath(symlink)
287  # llvm
288  package = os.path.basename(os.path.dirname(symlink))
289  if not package:
290    raise ValueError('Tried to uprev an unknown package')
291  # llvm
292  if package == 'llvm':
293    new_ebuild, is_changed = re.subn(
294        r'pre([0-9]+)_p([0-9]+)',
295        'pre%s_p%s' % (svn_version, \
296            datetime.today().strftime('%Y%m%d')),
297        ebuild,
298        count=1)
299  # any other package
300  else:
301    new_ebuild, is_changed = re.subn(
302        r'pre([0-9]+)', 'pre%s' % svn_version, ebuild, count=1)
303
304  if not is_changed:  # failed to increment the revision number
305    raise ValueError('Failed to uprev the ebuild.')
306
307  symlink_dir = os.path.dirname(symlink)
308
309  # Rename the ebuild
310  subprocess.check_output(['git', '-C', symlink_dir, 'mv', ebuild, new_ebuild])
311
312  # Create a symlink of the renamed ebuild
313  new_symlink = new_ebuild[:-len('.ebuild')] + '-r1.ebuild'
314  subprocess.check_output(['ln', '-s', '-r', new_ebuild, new_symlink])
315
316  if not os.path.islink(new_symlink):
317    raise ValueError('Invalid symlink name: %s' % new_ebuild[:-len('.ebuild')])
318
319  subprocess.check_output(['git', '-C', symlink_dir, 'add', new_symlink])
320
321  # Remove the old symlink
322  subprocess.check_output(['git', '-C', symlink_dir, 'rm', symlink])
323
324
325def CreatePathDictionaryFromPackages(chroot_path, update_packages):
326  """Creates a symlink and ebuild path pair dictionary from the packages.
327
328  Args:
329    chroot_path: The absolute path to the chroot.
330    update_packages: The filtered packages to be updated.
331
332  Returns:
333    A dictionary where the key is the absolute path to the symlink
334    of the package and the value is the absolute path to the ebuild of
335    the package.
336  """
337
338  # Construct a list containing the chroot file paths of the package(s).
339  chroot_file_paths = chroot.GetChrootEbuildPaths(chroot_path, update_packages)
340
341  # Construct a list containing the symlink(s) of the package(s).
342  symlink_file_paths = chroot.ConvertChrootPathsToAbsolutePaths(
343      chroot_path, chroot_file_paths)
344
345  # Create a dictionary where the key is the absolute path of the symlink to
346  # the package and the value is the absolute path to the ebuild of the package.
347  return GetEbuildPathsFromSymLinkPaths(symlink_file_paths)
348
349
350def RemovePatchesFromFilesDir(patches):
351  """Removes the patches from $FILESDIR of a package.
352
353  Args:
354    patches: A list of absolute pathes of patches to remove
355
356  Raises:
357    ValueError: Failed to remove a patch in $FILESDIR.
358  """
359
360  for patch in patches:
361    subprocess.check_output(
362        ['git', '-C', os.path.dirname(patch), 'rm', '-f', patch])
363
364
365def StagePatchMetadataFileForCommit(patch_metadata_file_path):
366  """Stages the updated patch metadata file for commit.
367
368  Args:
369    patch_metadata_file_path: The absolute path to the patch metadata file.
370
371  Raises:
372    ValueError: Failed to stage the patch metadata file for commit or invalid
373    patch metadata file.
374  """
375
376  if not os.path.isfile(patch_metadata_file_path):
377    raise ValueError(
378        'Invalid patch metadata file provided: %s' % patch_metadata_file_path)
379
380  # Cmd to stage the patch metadata file for commit.
381  subprocess.check_output([
382      'git', '-C',
383      os.path.dirname(patch_metadata_file_path), 'add', patch_metadata_file_path
384  ])
385
386
387def StagePackagesPatchResultsForCommit(package_info_dict, commit_messages):
388  """Stages the patch results of the packages to the commit message.
389
390  Args:
391    package_info_dict: A dictionary where the key is the package name and the
392    value is a dictionary that contains information about the patches of the
393    package (key).
394    commit_messages: The commit message that has the updated ebuilds and
395    upreving information.
396  """
397
398  # For each package, check if any patches for that package have
399  # changed, if so, add which patches have changed to the commit
400  # message.
401  for package_name, patch_info_dict in package_info_dict.items():
402    if patch_info_dict['disabled_patches'] or \
403        patch_info_dict['removed_patches'] or \
404        patch_info_dict['modified_metadata']:
405      cur_package_header = '\nFor the package %s:' % package_name
406      commit_messages.append(cur_package_header)
407
408    # Add to the commit message that the patch metadata file was modified.
409    if patch_info_dict['modified_metadata']:
410      patch_metadata_path = patch_info_dict['modified_metadata']
411      commit_messages.append('The patch metadata file %s was modified' %
412                             os.path.basename(patch_metadata_path))
413
414      StagePatchMetadataFileForCommit(patch_metadata_path)
415
416    # Add each disabled patch to the commit message.
417    if patch_info_dict['disabled_patches']:
418      commit_messages.append('The following patches were disabled:')
419
420      for patch_path in patch_info_dict['disabled_patches']:
421        commit_messages.append(os.path.basename(patch_path))
422
423    # Add each removed patch to the commit message.
424    if patch_info_dict['removed_patches']:
425      commit_messages.append('The following patches were removed:')
426
427      for patch_path in patch_info_dict['removed_patches']:
428        commit_messages.append(os.path.basename(patch_path))
429
430      RemovePatchesFromFilesDir(patch_info_dict['removed_patches'])
431
432  return commit_messages
433
434
435def UpdatePackages(packages, llvm_variant, git_hash, svn_version, chroot_path,
436                   patch_metadata_file, mode, git_hash_source,
437                   extra_commit_msg):
438  """Updates an LLVM hash and uprevs the ebuild of the packages.
439
440  A temporary repo is created for the changes. The changes are
441  then uploaded for review.
442
443  Args:
444    packages: A list of all the packages that are going to be updated.
445    llvm_variant: The LLVM hash to update.
446    git_hash: The new git hash.
447    svn_version: The SVN-style revision number of git_hash.
448    chroot_path: The absolute path to the chroot.
449    patch_metadata_file: The name of the .json file in '$FILESDIR/' that has
450    the patches and its metadata.
451    mode: The mode of the patch manager when handling an applicable patch
452    that failed to apply.
453      Ex: 'FailureModes.FAIL'
454    git_hash_source: The source of which git hash to use based off of.
455      Ex: 'google3', 'tot', or <version> such as 365123
456    extra_commit_msg: extra test to append to the commit message.
457
458  Returns:
459    A nametuple that has two (key, value) pairs, where the first pair is the
460    Gerrit commit URL and the second pair is the change list number.
461  """
462
463  # Determines whether to print the result of each executed command.
464  llvm_patch_management.verbose = verbose
465
466  # Construct a dictionary where the key is the absolute path of the symlink to
467  # the package and the value is the absolute path to the ebuild of the package.
468  paths_dict = CreatePathDictionaryFromPackages(chroot_path, packages)
469
470  repo_path = os.path.dirname(next(iter(paths_dict.values())))
471
472  branch = 'update-' + llvm_variant.value + '-' + git_hash
473
474  git.CreateBranch(repo_path, branch)
475
476  try:
477    commit_message_header = 'llvm'
478    if llvm_variant == LLVMVariant.next:
479      commit_message_header = 'llvm-next'
480    if git_hash_source in get_llvm_hash.KNOWN_HASH_SOURCES:
481      commit_message_header += (
482          '/%s: upgrade to %s (r%d)' % (git_hash_source, git_hash, svn_version))
483    else:
484      commit_message_header += (
485          ': upgrade to %s (r%d)' % (git_hash, svn_version))
486
487    commit_messages = [
488        commit_message_header + '\n',
489        'The following packages have been updated:',
490    ]
491
492    # Holds the list of packages that are updating.
493    packages = []
494
495    # Iterate through the dictionary.
496    #
497    # For each iteration:
498    # 1) Update the ebuild's LLVM hash.
499    # 2) Uprev the ebuild (symlink).
500    # 3) Add the modified package to the commit message.
501    for symlink_path, ebuild_path in paths_dict.items():
502      path_to_ebuild_dir = os.path.dirname(ebuild_path)
503
504      UpdateEbuildLLVMHash(ebuild_path, llvm_variant, git_hash, svn_version)
505
506      if llvm_variant == LLVMVariant.current:
507        UprevEbuildToVersion(symlink_path, svn_version)
508      else:
509        UprevEbuildSymlink(symlink_path)
510
511      cur_dir_name = os.path.basename(path_to_ebuild_dir)
512      parent_dir_name = os.path.basename(os.path.dirname(path_to_ebuild_dir))
513
514      packages.append('%s/%s' % (parent_dir_name, cur_dir_name))
515      commit_messages.append('%s/%s' % (parent_dir_name, cur_dir_name))
516
517    # Handle the patches for each package.
518    package_info_dict = llvm_patch_management.UpdatePackagesPatchMetadataFile(
519        chroot_path, svn_version, patch_metadata_file, packages, mode)
520
521    # Update the commit message if changes were made to a package's patches.
522    commit_messages = StagePackagesPatchResultsForCommit(
523        package_info_dict, commit_messages)
524
525    if extra_commit_msg:
526      commit_messages.append(extra_commit_msg)
527
528    change_list = git.UploadChanges(repo_path, branch, commit_messages)
529
530  finally:
531    git.DeleteBranch(repo_path, branch)
532
533  return change_list
534
535
536def main():
537  """Updates the LLVM next hash for each package.
538
539  Raises:
540    AssertionError: The script was run inside the chroot.
541  """
542
543  chroot.VerifyOutsideChroot()
544
545  args_output = GetCommandLineArgs()
546
547  llvm_variant = LLVMVariant.current
548  if args_output.is_llvm_next:
549    llvm_variant = LLVMVariant.next
550
551  git_hash_source = args_output.llvm_version
552
553  git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(
554      git_hash_source)
555
556  change_list = UpdatePackages(
557      args_output.update_packages,
558      llvm_variant,
559      git_hash,
560      svn_version,
561      args_output.chroot_path,
562      args_output.patch_metadata_file,
563      FailureModes(args_output.failure_mode),
564      git_hash_source,
565      extra_commit_msg=None)
566
567  print('Successfully updated packages to %s (%d)' % (git_hash, svn_version))
568  print('Gerrit URL: %s' % change_list.url)
569  print('Change list number: %d' % change_list.cl_number)
570
571
572if __name__ == '__main__':
573  main()
574