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