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