1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2020 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"""Tool to automatically generate a new Rust uprev CL. 8 9This tool is intended to automatically generate a CL to uprev Rust to a 10newer version in Chrome OS, including creating a new Rust version or 11removing an old version. It's based on 12src/third_party/chromiumos-overlay/dev-lang/rust/UPGRADE.md. When using 13the tool, the progress can be saved to a JSON file, so the user can resume 14the process after a failing step is fixed. Example usage to create a new 15version: 16 171. (inside chroot) $ ./rust_tools/rust_uprev.py 18 --state_file /tmp/state-file.json 19 create --rust_version 1.45.0 202. Step "compile rust" failed due to the patches can't apply to new version 213. Manually fix the patches 224. Execute the command in step 1 again. 235. Iterate 1-4 for each failed step until the tool passes. 24 25Replace `create --rust_version 1.45.0` with `remove --rust_version 1.43.0` 26if you want to remove all 1.43.0 related stuff in the same CL. Remember to 27use a different state file if you choose to run different subcommands. 28 29If you want a hammer that can do everything for you, use the subcommand 30`roll`. It can create a Rust uprev CL with `create` and `remove` and upload 31the CL to chromium code review. 32 33See `--help` for all available options. 34""" 35 36import argparse 37import pathlib 38import json 39import logging 40import os 41import re 42import shutil 43import subprocess 44import sys 45from pathlib import Path 46from typing import Any, Callable, Dict, List, NamedTuple, Optional, T, Tuple 47 48from llvm_tools import chroot, git 49 50EQUERY = 'equery' 51GSUTIL = 'gsutil.py' 52MIRROR_PATH = 'gs://chromeos-localmirror/distfiles' 53RUST_PATH = Path( 54 '/mnt/host/source/src/third_party/chromiumos-overlay/dev-lang/rust') 55 56 57def get_command_output(command: List[str], *args, **kwargs) -> str: 58 return subprocess.check_output(command, encoding='utf-8', *args, 59 **kwargs).strip() 60 61 62def get_command_output_unchecked(command: List[str], *args, **kwargs) -> str: 63 return subprocess.run(command, 64 check=False, 65 stdout=subprocess.PIPE, 66 encoding='utf-8', 67 *args, 68 **kwargs).stdout.strip() 69 70 71class RustVersion(NamedTuple): 72 """NamedTuple represents a Rust version""" 73 major: int 74 minor: int 75 patch: int 76 77 def __str__(self): 78 return f'{self.major}.{self.minor}.{self.patch}' 79 80 @staticmethod 81 def parse_from_ebuild(ebuild_name: str) -> 'RustVersion': 82 input_re = re.compile(r'^rust-' 83 r'(?P<major>\d+)\.' 84 r'(?P<minor>\d+)\.' 85 r'(?P<patch>\d+)' 86 r'(:?-r\d+)?' 87 r'\.ebuild$') 88 m = input_re.match(ebuild_name) 89 assert m, f'failed to parse {ebuild_name!r}' 90 return RustVersion(int(m.group('major')), int(m.group('minor')), 91 int(m.group('patch'))) 92 93 @staticmethod 94 def parse(x: str) -> 'RustVersion': 95 input_re = re.compile(r'^(?:rust-)?' 96 r'(?P<major>\d+)\.' 97 r'(?P<minor>\d+)\.' 98 r'(?P<patch>\d+)' 99 r'(?:.ebuild)?$') 100 m = input_re.match(x) 101 assert m, f'failed to parse {x!r}' 102 return RustVersion(int(m.group('major')), int(m.group('minor')), 103 int(m.group('patch'))) 104 105 106def compute_rustc_src_name(version: RustVersion) -> str: 107 return f'rustc-{version}-src.tar.gz' 108 109 110def compute_rust_bootstrap_prebuilt_name(version: RustVersion) -> str: 111 return f'rust-bootstrap-{version}.tbz2' 112 113 114def find_ebuild_for_package(name: str) -> os.PathLike: 115 """Returns the path to the ebuild for the named package.""" 116 return get_command_output([EQUERY, 'w', name]) 117 118 119def find_ebuild_path(directory: Path, 120 name: str, 121 version: Optional[RustVersion] = None) -> Path: 122 """Finds an ebuild in a directory. 123 124 Returns the path to the ebuild file. Asserts if there is not 125 exactly one match. The match is constrained by name and optionally 126 by version, but can match any patch level. E.g. "rust" version 127 1.3.4 can match rust-1.3.4.ebuild but also rust-1.3.4-r6.ebuild. 128 """ 129 if version: 130 pattern = f'{name}-{version}*.ebuild' 131 else: 132 pattern = f'{name}-*.ebuild' 133 matches = list(Path(directory).glob(pattern)) 134 assert len(matches) == 1, matches 135 return matches[0] 136 137 138def get_rust_bootstrap_version(): 139 """Get the version of the current rust-bootstrap package.""" 140 bootstrap_ebuild = find_ebuild_path(rust_bootstrap_path(), 'rust-bootstrap') 141 m = re.match(r'^rust-bootstrap-(\d+).(\d+).(\d+)', bootstrap_ebuild.name) 142 assert m, bootstrap_ebuild.name 143 return RustVersion(int(m.group(1)), int(m.group(2)), int(m.group(3))) 144 145 146def parse_commandline_args() -> argparse.Namespace: 147 parser = argparse.ArgumentParser( 148 description=__doc__, 149 formatter_class=argparse.RawDescriptionHelpFormatter) 150 parser.add_argument( 151 '--state_file', 152 required=True, 153 help='A state file to hold previous completed steps. If the file ' 154 'exists, it needs to be used together with --continue or --restart. ' 155 'If not exist (do not use --continue in this case), we will create a ' 156 'file for you.', 157 ) 158 parser.add_argument( 159 '--restart', 160 action='store_true', 161 help='Restart from the first step. Ignore the completed steps in ' 162 'the state file', 163 ) 164 parser.add_argument( 165 '--continue', 166 dest='cont', 167 action='store_true', 168 help='Continue the steps from the state file', 169 ) 170 171 create_parser_template = argparse.ArgumentParser(add_help=False) 172 create_parser_template.add_argument( 173 '--template', 174 type=RustVersion.parse, 175 default=None, 176 help='A template to use for creating a Rust uprev from, in the form ' 177 'a.b.c The ebuild has to exist in the chroot. If not specified, the ' 178 'tool will use the current Rust version in the chroot as template.', 179 ) 180 create_parser_template.add_argument( 181 '--skip_compile', 182 action='store_true', 183 help='Skip compiling rust to test the tool. Only for testing', 184 ) 185 186 subparsers = parser.add_subparsers(dest='subparser_name') 187 subparser_names = [] 188 subparser_names.append('create') 189 create_parser = subparsers.add_parser( 190 'create', 191 parents=[create_parser_template], 192 help='Create changes uprevs Rust to a new version', 193 ) 194 create_parser.add_argument( 195 '--rust_version', 196 type=RustVersion.parse, 197 required=True, 198 help='Rust version to uprev to, in the form a.b.c', 199 ) 200 201 subparser_names.append('remove') 202 remove_parser = subparsers.add_parser( 203 'remove', 204 help='Clean up old Rust version from chroot', 205 ) 206 remove_parser.add_argument( 207 '--rust_version', 208 type=RustVersion.parse, 209 default=None, 210 help='Rust version to remove, in the form a.b.c If not ' 211 'specified, the tool will remove the oldest version in the chroot', 212 ) 213 214 subparser_names.append('remove-bootstrap') 215 remove_bootstrap_parser = subparsers.add_parser( 216 'remove-bootstrap', 217 help='Remove an old rust-bootstrap version', 218 ) 219 remove_bootstrap_parser.add_argument( 220 '--version', 221 type=RustVersion.parse, 222 required=True, 223 help='rust-bootstrap version to remove', 224 ) 225 226 subparser_names.append('roll') 227 roll_parser = subparsers.add_parser( 228 'roll', 229 parents=[create_parser_template], 230 help='A command can create and upload a Rust uprev CL, including ' 231 'preparing the repo, creating new Rust uprev, deleting old uprev, ' 232 'and upload a CL to crrev.', 233 ) 234 roll_parser.add_argument( 235 '--uprev', 236 type=RustVersion.parse, 237 required=True, 238 help='Rust version to uprev to, in the form a.b.c', 239 ) 240 roll_parser.add_argument( 241 '--remove', 242 type=RustVersion.parse, 243 default=None, 244 help='Rust version to remove, in the form a.b.c If not ' 245 'specified, the tool will remove the oldest version in the chroot', 246 ) 247 roll_parser.add_argument( 248 '--skip_cross_compiler', 249 action='store_true', 250 help='Skip updating cross-compiler in the chroot', 251 ) 252 roll_parser.add_argument( 253 '--no_upload', 254 action='store_true', 255 help='If specified, the tool will not upload the CL for review', 256 ) 257 258 args = parser.parse_args() 259 if args.subparser_name not in subparser_names: 260 parser.error('one of %s must be specified' % subparser_names) 261 262 if args.cont and args.restart: 263 parser.error('Please select either --continue or --restart') 264 265 if os.path.exists(args.state_file): 266 if not args.cont and not args.restart: 267 parser.error('State file exists, so you should either --continue ' 268 'or --restart') 269 if args.cont and not os.path.exists(args.state_file): 270 parser.error('Indicate --continue but the state file does not exist') 271 272 if args.restart and os.path.exists(args.state_file): 273 os.remove(args.state_file) 274 275 return args 276 277 278def prepare_uprev(rust_version: RustVersion, template: Optional[RustVersion] 279 ) -> Optional[Tuple[RustVersion, str, RustVersion]]: 280 if template is None: 281 ebuild_path = find_ebuild_for_package('rust') 282 ebuild_name = os.path.basename(ebuild_path) 283 template_version = RustVersion.parse_from_ebuild(ebuild_name) 284 else: 285 ebuild_path = find_ebuild_for_rust_version(template) 286 template_version = template 287 288 bootstrap_version = get_rust_bootstrap_version() 289 290 if rust_version <= template_version: 291 logging.info( 292 'Requested version %s is not newer than the template version %s.', 293 rust_version, template_version) 294 return None 295 296 logging.info('Template Rust version is %s (ebuild: %r)', template_version, 297 ebuild_path) 298 logging.info('rust-bootstrap version is %s', bootstrap_version) 299 300 return template_version, ebuild_path, bootstrap_version 301 302 303def copy_patches(directory: Path, template_version: RustVersion, 304 new_version: RustVersion) -> None: 305 patch_path = directory.joinpath('files') 306 prefix = '%s-%s-' % (directory.name, template_version) 307 new_prefix = '%s-%s-' % (directory.name, new_version) 308 for f in os.listdir(patch_path): 309 if not f.startswith(prefix): 310 continue 311 logging.info('Copy patch %s to new version', f) 312 new_name = f.replace(str(template_version), str(new_version)) 313 shutil.copyfile( 314 os.path.join(patch_path, f), 315 os.path.join(patch_path, new_name), 316 ) 317 318 subprocess.check_call(['git', 'add', f'{new_prefix}*.patch'], cwd=patch_path) 319 320 321def create_ebuild(template_ebuild: str, new_version: RustVersion) -> str: 322 shutil.copyfile(template_ebuild, 323 RUST_PATH.joinpath(f'rust-{new_version}.ebuild')) 324 subprocess.check_call(['git', 'add', f'rust-{new_version}.ebuild'], 325 cwd=RUST_PATH) 326 return os.path.join(RUST_PATH, f'rust-{new_version}.ebuild') 327 328 329def update_bootstrap_ebuild(new_bootstrap_version: RustVersion) -> None: 330 old_ebuild = find_ebuild_path(rust_bootstrap_path(), 'rust-bootstrap') 331 m = re.match(r'^rust-bootstrap-(\d+).(\d+).(\d+)', old_ebuild.name) 332 assert m, old_ebuild.name 333 old_version = RustVersion(m.group(1), m.group(2), m.group(3)) 334 new_ebuild = old_ebuild.parent.joinpath( 335 f'rust-bootstrap-{new_bootstrap_version}.ebuild') 336 old_text = old_ebuild.read_text(encoding='utf-8') 337 new_text, changes = re.subn(r'(RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=\([^)]*)', 338 f'\\1\t{old_version}\n', 339 old_text, 340 flags=re.MULTILINE) 341 assert changes == 1, 'Failed to update RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE' 342 new_ebuild.write_text(new_text, encoding='utf-8') 343 344 345def update_ebuild(ebuild_file: str, 346 new_bootstrap_version: RustVersion) -> None: 347 contents = open(ebuild_file, encoding='utf-8').read() 348 contents, subs = re.subn(r'^BOOTSTRAP_VERSION=.*$', 349 'BOOTSTRAP_VERSION="%s"' % 350 (new_bootstrap_version, ), 351 contents, 352 flags=re.MULTILINE) 353 if not subs: 354 raise RuntimeError('BOOTSTRAP_VERSION not found in rust ebuild') 355 open(ebuild_file, 'w', encoding='utf-8').write(contents) 356 logging.info('Rust ebuild file has BOOTSTRAP_VERSION updated to %s', 357 new_bootstrap_version) 358 359 360def flip_mirror_in_ebuild(ebuild_file: Path, add: bool) -> None: 361 restrict_re = re.compile( 362 r'(?P<before>RESTRICT=")(?P<values>"[^"]*"|.*)(?P<after>")') 363 with open(ebuild_file, encoding='utf-8') as f: 364 contents = f.read() 365 m = restrict_re.search(contents) 366 assert m, 'failed to find RESTRICT variable in Rust ebuild' 367 values = m.group('values') 368 if add: 369 if 'mirror' in values: 370 return 371 values += ' mirror' 372 else: 373 if 'mirror' not in values: 374 return 375 values = values.replace(' mirror', '') 376 new_contents = restrict_re.sub(r'\g<before>%s\g<after>' % values, contents) 377 with open(ebuild_file, 'w', encoding='utf-8') as f: 378 f.write(new_contents) 379 380 381def ebuild_actions(package: str, actions: List[str], 382 sudo: bool = False) -> None: 383 ebuild_path_inchroot = find_ebuild_for_package(package) 384 cmd = ['ebuild', ebuild_path_inchroot] + actions 385 if sudo: 386 cmd = ['sudo'] + cmd 387 subprocess.check_call(cmd) 388 389 390def fetch_distfile_from_mirror(name: str) -> None: 391 """Gets the named file from the local mirror. 392 393 This ensures that the file exists on the mirror, and 394 that we can read it. We overwrite any existing distfile 395 to ensure the checksums that update_manifest() records 396 match the file as it exists on the mirror. 397 398 This function also attempts to verify the ACL for 399 the file (which is expected to have READER permission 400 for allUsers). We can only see the ACL if the user 401 gsutil runs with is the owner of the file. If not, 402 we get an access denied error. We also count this 403 as a success, because it means we were able to fetch 404 the file even though we don't own it. 405 """ 406 mirror_file = MIRROR_PATH + '/' + name 407 local_file = Path(get_distdir(), name) 408 cmd = [GSUTIL, 'cp', mirror_file, local_file] 409 logging.info('Running %r', cmd) 410 rc = subprocess.call(cmd) 411 if rc != 0: 412 logging.error( 413 """Could not fetch %s 414 415If the file does not yet exist at %s 416please download the file, verify its integrity 417with something like: 418 419curl -O https://static.rust-lang.org/dist/%s 420gpg --verify %s.asc 421 422You may need to import the signing key first, e.g.: 423 424gpg --recv-keys 85AB96E6FA1BE5FE 425 426Once you have verify the integrity of the file, upload 427it to the local mirror using gsutil cp. 428""", mirror_file, MIRROR_PATH, name, name) 429 raise Exception(f'Could not fetch {mirror_file}') 430 # Check that the ACL allows allUsers READER access. 431 # If we get an AccessDeniedAcception here, that also 432 # counts as a success, because we were able to fetch 433 # the file as a non-owner. 434 cmd = [GSUTIL, 'acl', 'get', mirror_file] 435 logging.info('Running %r', cmd) 436 output = get_command_output_unchecked(cmd, stderr=subprocess.STDOUT) 437 acl_verified = False 438 if 'AccessDeniedException:' in output: 439 acl_verified = True 440 else: 441 acl = json.loads(output) 442 for x in acl: 443 if x['entity'] == 'allUsers' and x['role'] == 'READER': 444 acl_verified = True 445 break 446 if not acl_verified: 447 logging.error('Output from acl get:\n%s', output) 448 raise Exception('Could not verify that allUsers has READER permission') 449 450 451def fetch_bootstrap_distfiles(old_version: RustVersion, 452 new_version: RustVersion) -> None: 453 """Fetches rust-bootstrap distfiles from the local mirror 454 455 Fetches the distfiles for a rust-bootstrap ebuild to ensure they 456 are available on the mirror and the local copies are the same as 457 the ones on the mirror. 458 """ 459 fetch_distfile_from_mirror(compute_rust_bootstrap_prebuilt_name(old_version)) 460 fetch_distfile_from_mirror(compute_rustc_src_name(new_version)) 461 462 463def fetch_rust_distfiles(version: RustVersion) -> None: 464 """Fetches rust distfiles from the local mirror 465 466 Fetches the distfiles for a rust ebuild to ensure they 467 are available on the mirror and the local copies are 468 the same as the ones on the mirror. 469 """ 470 fetch_distfile_from_mirror(compute_rustc_src_name(version)) 471 472 473def get_distdir() -> os.PathLike: 474 """Returns portage's distdir.""" 475 return get_command_output(['portageq', 'distdir']) 476 477 478def update_manifest(ebuild_file: os.PathLike) -> None: 479 """Updates the MANIFEST for the ebuild at the given path.""" 480 ebuild = Path(ebuild_file) 481 logging.info('Added "mirror" to RESTRICT to %s', ebuild.name) 482 flip_mirror_in_ebuild(ebuild, add=True) 483 ebuild_actions(ebuild.parent.name, ['manifest']) 484 logging.info('Removed "mirror" to RESTRICT from %s', ebuild.name) 485 flip_mirror_in_ebuild(ebuild, add=False) 486 487 488def update_rust_packages(rust_version: RustVersion, add: bool) -> None: 489 package_file = RUST_PATH.joinpath( 490 '../../profiles/targets/chromeos/package.provided') 491 with open(package_file, encoding='utf-8') as f: 492 contents = f.read() 493 if add: 494 rust_packages_re = re.compile(r'dev-lang/rust-(\d+\.\d+\.\d+)') 495 rust_packages = rust_packages_re.findall(contents) 496 # Assume all the rust packages are in alphabetical order, so insert the new 497 # version to the place after the last rust_packages 498 new_str = f'dev-lang/rust-{rust_version}' 499 new_contents = contents.replace(rust_packages[-1], 500 f'{rust_packages[-1]}\n{new_str}') 501 logging.info('%s has been inserted into package.provided', new_str) 502 else: 503 old_str = f'dev-lang/rust-{rust_version}\n' 504 assert old_str in contents, f'{old_str!r} not found in package.provided' 505 new_contents = contents.replace(old_str, '') 506 logging.info('%s has been removed from package.provided', old_str) 507 508 with open(package_file, 'w', encoding='utf-8') as f: 509 f.write(new_contents) 510 511 512def update_virtual_rust(template_version: RustVersion, 513 new_version: RustVersion) -> None: 514 template_ebuild = find_ebuild_path(RUST_PATH.joinpath('../../virtual/rust'), 515 'rust', template_version) 516 virtual_rust_dir = template_ebuild.parent 517 new_name = f'rust-{new_version}.ebuild' 518 new_ebuild = virtual_rust_dir.joinpath(new_name) 519 shutil.copyfile(template_ebuild, new_ebuild) 520 subprocess.check_call(['git', 'add', new_name], cwd=virtual_rust_dir) 521 522 523def perform_step(state_file: pathlib.Path, 524 tmp_state_file: pathlib.Path, 525 completed_steps: Dict[str, Any], 526 step_name: str, 527 step_fn: Callable[[], T], 528 result_from_json: Optional[Callable[[Any], T]] = None, 529 result_to_json: Optional[Callable[[T], Any]] = None) -> T: 530 if step_name in completed_steps: 531 logging.info('Skipping previously completed step %s', step_name) 532 if result_from_json: 533 return result_from_json(completed_steps[step_name]) 534 return completed_steps[step_name] 535 536 logging.info('Running step %s', step_name) 537 val = step_fn() 538 logging.info('Step %s complete', step_name) 539 if result_to_json: 540 completed_steps[step_name] = result_to_json(val) 541 else: 542 completed_steps[step_name] = val 543 544 with tmp_state_file.open('w', encoding='utf-8') as f: 545 json.dump(completed_steps, f, indent=4) 546 tmp_state_file.rename(state_file) 547 return val 548 549 550def prepare_uprev_from_json( 551 obj: Any) -> Optional[Tuple[RustVersion, str, RustVersion]]: 552 if not obj: 553 return None 554 version, ebuild_path, bootstrap_version = obj 555 return RustVersion(*version), ebuild_path, RustVersion(*bootstrap_version) 556 557 558def create_rust_uprev(rust_version: RustVersion, 559 maybe_template_version: Optional[RustVersion], 560 skip_compile: bool, run_step: Callable[[], T]) -> None: 561 template_version, template_ebuild, old_bootstrap_version = run_step( 562 'prepare uprev', 563 lambda: prepare_uprev(rust_version, maybe_template_version), 564 result_from_json=prepare_uprev_from_json, 565 ) 566 if template_ebuild is None: 567 return 568 569 # The fetch steps will fail (on purpose) if the files they check for 570 # are not available on the mirror. To make them pass, fetch the 571 # required files yourself, verify their checksums, then upload them 572 # to the mirror. 573 run_step( 574 'fetch bootstrap distfiles', lambda: fetch_bootstrap_distfiles( 575 old_bootstrap_version, template_version)) 576 run_step('fetch rust distfiles', lambda: fetch_rust_distfiles(rust_version)) 577 run_step('update bootstrap ebuild', lambda: update_bootstrap_ebuild( 578 template_version)) 579 run_step( 580 'update bootstrap manifest', lambda: update_manifest(rust_bootstrap_path( 581 ).joinpath(f'rust-bootstrap-{template_version}.ebuild'))) 582 run_step('copy patches', lambda: copy_patches(RUST_PATH, template_version, 583 rust_version)) 584 ebuild_file = run_step( 585 'create ebuild', lambda: create_ebuild(template_ebuild, rust_version)) 586 run_step( 587 'update ebuild', lambda: update_ebuild(ebuild_file, template_version)) 588 run_step('update manifest to add new version', lambda: update_manifest( 589 Path(ebuild_file))) 590 if not skip_compile: 591 run_step( 592 'emerge rust', lambda: subprocess.check_call( 593 ['sudo', 'emerge', 'dev-lang/rust'])) 594 run_step('insert version into rust packages', lambda: update_rust_packages( 595 rust_version, add=True)) 596 run_step('upgrade virtual/rust', lambda: update_virtual_rust( 597 template_version, rust_version)) 598 599 600def find_rust_versions_in_chroot() -> List[Tuple[RustVersion, str]]: 601 return [(RustVersion.parse_from_ebuild(x), os.path.join(RUST_PATH, x)) 602 for x in os.listdir(RUST_PATH) if x.endswith('.ebuild')] 603 604 605def find_oldest_rust_version_in_chroot() -> Tuple[RustVersion, str]: 606 rust_versions = find_rust_versions_in_chroot() 607 if len(rust_versions) <= 1: 608 raise RuntimeError('Expect to find more than one Rust versions') 609 return min(rust_versions) 610 611 612def find_ebuild_for_rust_version(version: RustVersion) -> str: 613 rust_ebuilds = [ 614 ebuild for x, ebuild in find_rust_versions_in_chroot() if x == version 615 ] 616 if not rust_ebuilds: 617 raise ValueError(f'No Rust ebuilds found matching {version}') 618 if len(rust_ebuilds) > 1: 619 raise ValueError(f'Multiple Rust ebuilds found matching {version}: ' 620 f'{rust_ebuilds}') 621 return rust_ebuilds[0] 622 623 624def remove_files(filename: str, path: str) -> None: 625 subprocess.check_call(['git', 'rm', filename], cwd=path) 626 627 628def remove_rust_bootstrap_version(version: RustVersion, 629 run_step: Callable[[], T]) -> None: 630 prefix = f'rust-bootstrap-{version}' 631 run_step('remove old bootstrap ebuild', lambda: remove_files( 632 f'{prefix}*.ebuild', rust_bootstrap_path())) 633 ebuild_file = find_ebuild_for_package('rust-bootstrap') 634 run_step('update bootstrap manifest to delete old version', lambda: 635 update_manifest(ebuild_file)) 636 637 638def remove_rust_uprev(rust_version: Optional[RustVersion], 639 run_step: Callable[[], T]) -> None: 640 def find_desired_rust_version(): 641 if rust_version: 642 return rust_version, find_ebuild_for_rust_version(rust_version) 643 return find_oldest_rust_version_in_chroot() 644 645 def find_desired_rust_version_from_json(obj: Any) -> Tuple[RustVersion, str]: 646 version, ebuild_path = obj 647 return RustVersion(*version), ebuild_path 648 649 delete_version, delete_ebuild = run_step( 650 'find rust version to delete', 651 find_desired_rust_version, 652 result_from_json=find_desired_rust_version_from_json, 653 ) 654 run_step( 655 'remove patches', lambda: remove_files( 656 f'files/rust-{delete_version}-*.patch', RUST_PATH)) 657 run_step('remove ebuild', lambda: remove_files(delete_ebuild, RUST_PATH)) 658 ebuild_file = find_ebuild_for_package('rust') 659 run_step('update manifest to delete old version', lambda: update_manifest( 660 ebuild_file)) 661 run_step('remove version from rust packages', lambda: update_rust_packages( 662 delete_version, add=False)) 663 run_step('remove virtual/rust', lambda: remove_virtual_rust(delete_version)) 664 665 666def remove_virtual_rust(delete_version: RustVersion) -> None: 667 ebuild = find_ebuild_path(RUST_PATH.joinpath('../../virtual/rust'), 'rust', 668 delete_version) 669 subprocess.check_call(['git', 'rm', str(ebuild.name)], cwd=ebuild.parent) 670 671 672def rust_bootstrap_path() -> Path: 673 return RUST_PATH.joinpath('../rust-bootstrap') 674 675 676def create_new_repo(rust_version: RustVersion) -> None: 677 output = get_command_output(['git', 'status', '--porcelain'], cwd=RUST_PATH) 678 if output: 679 raise RuntimeError( 680 f'{RUST_PATH} has uncommitted changes, please either discard them ' 681 'or commit them.') 682 git.CreateBranch(RUST_PATH, f'rust-to-{rust_version}') 683 684 685def build_cross_compiler() -> None: 686 # Get target triples in ebuild 687 rust_ebuild = find_ebuild_for_package('rust') 688 with open(rust_ebuild, encoding='utf-8') as f: 689 contents = f.read() 690 691 target_triples_re = re.compile(r'RUSTC_TARGET_TRIPLES=\(([^)]+)\)') 692 m = target_triples_re.search(contents) 693 assert m, 'RUST_TARGET_TRIPLES not found in rust ebuild' 694 target_triples = m.group(1).strip().split('\n') 695 696 compiler_targets_to_install = [ 697 target.strip() for target in target_triples if 'cros-' in target 698 ] 699 for target in target_triples: 700 if 'cros-' not in target: 701 continue 702 target = target.strip() 703 704 # We also always need arm-none-eabi, though it's not mentioned in 705 # RUSTC_TARGET_TRIPLES. 706 compiler_targets_to_install.append('arm-none-eabi') 707 708 logging.info('Emerging cross compilers %s', compiler_targets_to_install) 709 subprocess.check_call( 710 ['sudo', 'emerge', '-j', '-G'] + 711 [f'cross-{target}/gcc' for target in compiler_targets_to_install]) 712 713 714def create_new_commit(rust_version: RustVersion) -> None: 715 subprocess.check_call(['git', 'add', '-A'], cwd=RUST_PATH) 716 messages = [ 717 f'[DO NOT SUBMIT] dev-lang/rust: upgrade to Rust {rust_version}', 718 '', 719 'This CL is created by rust_uprev tool automatically.' 720 '', 721 'BUG=None', 722 'TEST=Use CQ to test the new Rust version', 723 ] 724 git.UploadChanges(RUST_PATH, f'rust-to-{rust_version}', messages) 725 726 727def main() -> None: 728 if not chroot.InChroot(): 729 raise RuntimeError('This script must be executed inside chroot') 730 731 logging.basicConfig(level=logging.INFO) 732 733 args = parse_commandline_args() 734 735 state_file = pathlib.Path(args.state_file) 736 tmp_state_file = state_file.with_suffix('.tmp') 737 738 try: 739 with state_file.open(encoding='utf-8') as f: 740 completed_steps = json.load(f) 741 except FileNotFoundError: 742 completed_steps = {} 743 744 def run_step( 745 step_name: str, 746 step_fn: Callable[[], T], 747 result_from_json: Optional[Callable[[Any], T]] = None, 748 result_to_json: Optional[Callable[[T], Any]] = None, 749 ) -> T: 750 return perform_step(state_file, tmp_state_file, completed_steps, step_name, 751 step_fn, result_from_json, result_to_json) 752 753 if args.subparser_name == 'create': 754 create_rust_uprev(args.rust_version, args.template, args.skip_compile, 755 run_step) 756 elif args.subparser_name == 'remove': 757 remove_rust_uprev(args.rust_version, run_step) 758 elif args.subparser_name == 'remove-bootstrap': 759 remove_rust_bootstrap_version(args.version, run_step) 760 else: 761 # If you have added more subparser_name, please also add the handlers above 762 assert args.subparser_name == 'roll' 763 run_step('create new repo', lambda: create_new_repo(args.uprev)) 764 if not args.skip_cross_compiler: 765 run_step('build cross compiler', build_cross_compiler) 766 create_rust_uprev(args.uprev, args.template, args.skip_compile, run_step) 767 remove_rust_uprev(args.remove, run_step) 768 bootstrap_version = prepare_uprev_from_json( 769 completed_steps['prepare uprev'])[2] 770 remove_rust_bootstrap_version(bootstrap_version, run_step) 771 if not args.no_upload: 772 run_step('create rust uprev CL', lambda: create_new_commit(args.uprev)) 773 774 775if __name__ == '__main__': 776 sys.exit(main()) 777