1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2020 The ChromiumOS Authors 3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 4*760c253cSXin Li# found in the LICENSE file. 5*760c253cSXin Li 6*760c253cSXin Li"""Tool to automatically generate a new Rust uprev CL. 7*760c253cSXin Li 8*760c253cSXin LiThis tool is intended to automatically generate a CL to uprev Rust to 9*760c253cSXin Lia newer version in Chrome OS, including creating a new Rust version or 10*760c253cSXin Liremoving an old version. When using the tool, the progress can be 11*760c253cSXin Lisaved to a JSON file, so the user can resume the process after a 12*760c253cSXin Lifailing step is fixed. Example usage to create a new version: 13*760c253cSXin Li 14*760c253cSXin Li1. (outside chroot) $ ./rust_tools/rust_uprev.py \\ 15*760c253cSXin Li --state_file /tmp/rust-to-1.60.0.json \\ 16*760c253cSXin Li roll --uprev 1.60.0 17*760c253cSXin Li2. Step "compile rust" failed due to the patches can't apply to new version. 18*760c253cSXin Li3. Manually fix the patches. 19*760c253cSXin Li4. Execute the command in step 1 again, but add "--continue" before "roll". 20*760c253cSXin Li5. Iterate 1-4 for each failed step until the tool passes. 21*760c253cSXin Li 22*760c253cSXin LiBesides "roll", the tool also support subcommands that perform 23*760c253cSXin Livarious parts of an uprev. 24*760c253cSXin Li 25*760c253cSXin LiSee `--help` for all available options. 26*760c253cSXin Li""" 27*760c253cSXin Li 28*760c253cSXin Liimport argparse 29*760c253cSXin Liimport functools 30*760c253cSXin Liimport json 31*760c253cSXin Liimport logging 32*760c253cSXin Liimport os 33*760c253cSXin Liimport pathlib 34*760c253cSXin Lifrom pathlib import Path 35*760c253cSXin Liimport re 36*760c253cSXin Liimport shlex 37*760c253cSXin Liimport shutil 38*760c253cSXin Liimport subprocess 39*760c253cSXin Liimport threading 40*760c253cSXin Liimport time 41*760c253cSXin Lifrom typing import ( 42*760c253cSXin Li Any, 43*760c253cSXin Li Callable, 44*760c253cSXin Li Dict, 45*760c253cSXin Li List, 46*760c253cSXin Li NamedTuple, 47*760c253cSXin Li Optional, 48*760c253cSXin Li Protocol, 49*760c253cSXin Li Sequence, 50*760c253cSXin Li Tuple, 51*760c253cSXin Li TypeVar, 52*760c253cSXin Li Union, 53*760c253cSXin Li) 54*760c253cSXin Liimport urllib.request 55*760c253cSXin Li 56*760c253cSXin Lifrom llvm_tools import chroot 57*760c253cSXin Lifrom llvm_tools import git 58*760c253cSXin Li 59*760c253cSXin Li 60*760c253cSXin LiT = TypeVar("T") 61*760c253cSXin LiCommand = Sequence[Union[str, os.PathLike]] 62*760c253cSXin LiPathOrStr = Union[str, os.PathLike] 63*760c253cSXin Li 64*760c253cSXin Li 65*760c253cSXin Liclass RunStepFn(Protocol): 66*760c253cSXin Li """Protocol that corresponds to run_step's type. 67*760c253cSXin Li 68*760c253cSXin Li This can be used as the type of a function parameter that accepts 69*760c253cSXin Li run_step as its value. 70*760c253cSXin Li """ 71*760c253cSXin Li 72*760c253cSXin Li def __call__( 73*760c253cSXin Li self, 74*760c253cSXin Li step_name: str, 75*760c253cSXin Li step_fn: Callable[[], T], 76*760c253cSXin Li result_from_json: Optional[Callable[[Any], T]] = None, 77*760c253cSXin Li result_to_json: Optional[Callable[[T], Any]] = None, 78*760c253cSXin Li ) -> T: 79*760c253cSXin Li ... 80*760c253cSXin Li 81*760c253cSXin Li 82*760c253cSXin Lidef get_command_output(command: Command, *args, **kwargs) -> str: 83*760c253cSXin Li return subprocess.check_output( 84*760c253cSXin Li command, encoding="utf-8", *args, **kwargs 85*760c253cSXin Li ).strip() 86*760c253cSXin Li 87*760c253cSXin Li 88*760c253cSXin Lidef _get_source_root() -> Path: 89*760c253cSXin Li """Returns the path to the chromiumos directory.""" 90*760c253cSXin Li return Path(get_command_output(["repo", "--show-toplevel"])) 91*760c253cSXin Li 92*760c253cSXin Li 93*760c253cSXin LiSOURCE_ROOT = _get_source_root() 94*760c253cSXin LiEQUERY = "equery" 95*760c253cSXin LiGPG = "gpg" 96*760c253cSXin LiGSUTIL = "gsutil.py" 97*760c253cSXin LiMIRROR_PATH = "gs://chromeos-localmirror/distfiles" 98*760c253cSXin LiEBUILD_PREFIX = SOURCE_ROOT / "src/third_party/chromiumos-overlay" 99*760c253cSXin LiCROS_RUSTC_ECLASS = EBUILD_PREFIX / "eclass/cros-rustc.eclass" 100*760c253cSXin Li# Keyserver to use with GPG. Not all keyservers have Rust's signing key; 101*760c253cSXin Li# this must be set to a keyserver that does. 102*760c253cSXin LiGPG_KEYSERVER = "keyserver.ubuntu.com" 103*760c253cSXin LiPGO_RUST = Path( 104*760c253cSXin Li "/mnt/host/source" 105*760c253cSXin Li "/src/third_party/toolchain-utils/pgo_tools_rust/pgo_rust.py" 106*760c253cSXin Li) 107*760c253cSXin LiRUST_PATH = Path(EBUILD_PREFIX, "dev-lang", "rust") 108*760c253cSXin Li# This is the signing key used by upstream Rust as of 2023-08-09. 109*760c253cSXin Li# If the project switches to a different key, this will have to be updated. 110*760c253cSXin Li# We require the key to be updated manually so that we have an opportunity 111*760c253cSXin Li# to verify that the key change is legitimate. 112*760c253cSXin LiRUST_SIGNING_KEY = "85AB96E6FA1BE5FE" 113*760c253cSXin LiRUST_SRC_BASE_URI = "https://static.rust-lang.org/dist/" 114*760c253cSXin Li# Packages that need to be processed like dev-lang/rust. 115*760c253cSXin LiRUST_PACKAGES = ( 116*760c253cSXin Li ("dev-lang", "rust-host"), 117*760c253cSXin Li ("dev-lang", "rust"), 118*760c253cSXin Li) 119*760c253cSXin Li 120*760c253cSXin Li 121*760c253cSXin Liclass SignatureVerificationError(Exception): 122*760c253cSXin Li """Error that indicates verification of a downloaded file failed. 123*760c253cSXin Li 124*760c253cSXin Li Attributes: 125*760c253cSXin Li message: explanation of why the verification failed. 126*760c253cSXin Li path: the path to the file whose integrity was being verified. 127*760c253cSXin Li """ 128*760c253cSXin Li 129*760c253cSXin Li def __init__(self, message: str, path: Path): 130*760c253cSXin Li super(SignatureVerificationError, self).__init__() 131*760c253cSXin Li self.message = message 132*760c253cSXin Li self.path = path 133*760c253cSXin Li 134*760c253cSXin Li 135*760c253cSXin Lidef get_command_output_unchecked(command: Command, *args, **kwargs) -> str: 136*760c253cSXin Li # pylint: disable=subprocess-run-check 137*760c253cSXin Li return subprocess.run( 138*760c253cSXin Li command, 139*760c253cSXin Li *args, 140*760c253cSXin Li **dict( 141*760c253cSXin Li { 142*760c253cSXin Li "check": False, 143*760c253cSXin Li "stdout": subprocess.PIPE, 144*760c253cSXin Li "encoding": "utf-8", 145*760c253cSXin Li }, 146*760c253cSXin Li **kwargs, 147*760c253cSXin Li ), 148*760c253cSXin Li ).stdout.strip() 149*760c253cSXin Li 150*760c253cSXin Li 151*760c253cSXin Liclass RustVersion(NamedTuple): 152*760c253cSXin Li """NamedTuple represents a Rust version""" 153*760c253cSXin Li 154*760c253cSXin Li major: int 155*760c253cSXin Li minor: int 156*760c253cSXin Li patch: int 157*760c253cSXin Li 158*760c253cSXin Li def __str__(self): 159*760c253cSXin Li return f"{self.major}.{self.minor}.{self.patch}" 160*760c253cSXin Li 161*760c253cSXin Li @staticmethod 162*760c253cSXin Li def parse_from_ebuild(ebuild_name: PathOrStr) -> "RustVersion": 163*760c253cSXin Li input_re = re.compile( 164*760c253cSXin Li r"^rust-" 165*760c253cSXin Li r"(?P<major>\d+)\." 166*760c253cSXin Li r"(?P<minor>\d+)\." 167*760c253cSXin Li r"(?P<patch>\d+)" 168*760c253cSXin Li r"(:?-r\d+)?" 169*760c253cSXin Li r"\.ebuild$" 170*760c253cSXin Li ) 171*760c253cSXin Li m = input_re.match(Path(ebuild_name).name) 172*760c253cSXin Li assert m, f"failed to parse {ebuild_name!r}" 173*760c253cSXin Li return RustVersion( 174*760c253cSXin Li int(m.group("major")), int(m.group("minor")), int(m.group("patch")) 175*760c253cSXin Li ) 176*760c253cSXin Li 177*760c253cSXin Li @staticmethod 178*760c253cSXin Li def parse(x: str) -> "RustVersion": 179*760c253cSXin Li input_re = re.compile( 180*760c253cSXin Li r"^(?:rust-)?" 181*760c253cSXin Li r"(?P<major>\d+)\." 182*760c253cSXin Li r"(?P<minor>\d+)\." 183*760c253cSXin Li r"(?P<patch>\d+)" 184*760c253cSXin Li r"(?:.ebuild)?$" 185*760c253cSXin Li ) 186*760c253cSXin Li m = input_re.match(x) 187*760c253cSXin Li assert m, f"failed to parse {x!r}" 188*760c253cSXin Li return RustVersion( 189*760c253cSXin Li int(m.group("major")), int(m.group("minor")), int(m.group("patch")) 190*760c253cSXin Li ) 191*760c253cSXin Li 192*760c253cSXin Li 193*760c253cSXin Liclass PreparedUprev(NamedTuple): 194*760c253cSXin Li """Container for the information returned by prepare_uprev.""" 195*760c253cSXin Li 196*760c253cSXin Li template_version: RustVersion 197*760c253cSXin Li 198*760c253cSXin Li 199*760c253cSXin Lidef compute_ebuild_path(category: str, name: str, version: RustVersion) -> Path: 200*760c253cSXin Li return EBUILD_PREFIX / category / name / f"{name}-{version}.ebuild" 201*760c253cSXin Li 202*760c253cSXin Li 203*760c253cSXin Lidef compute_rustc_src_name(version: RustVersion) -> str: 204*760c253cSXin Li return f"rustc-{version}-src.tar.gz" 205*760c253cSXin Li 206*760c253cSXin Li 207*760c253cSXin Lidef find_ebuild_for_package(name: str) -> str: 208*760c253cSXin Li """Returns the path to the ebuild for the named package.""" 209*760c253cSXin Li return run_in_chroot( 210*760c253cSXin Li [EQUERY, "w", name], 211*760c253cSXin Li stdout=subprocess.PIPE, 212*760c253cSXin Li ).stdout.strip() 213*760c253cSXin Li 214*760c253cSXin Li 215*760c253cSXin Lidef find_ebuild_path( 216*760c253cSXin Li directory: Path, name: str, version: Optional[RustVersion] = None 217*760c253cSXin Li) -> Path: 218*760c253cSXin Li """Finds an ebuild in a directory. 219*760c253cSXin Li 220*760c253cSXin Li Returns the path to the ebuild file. The match is constrained by 221*760c253cSXin Li name and optionally by version, but can match any patch level. 222*760c253cSXin Li E.g. "rust" version 1.3.4 can match rust-1.3.4.ebuild but also 223*760c253cSXin Li rust-1.3.4-r6.ebuild. 224*760c253cSXin Li 225*760c253cSXin Li The expectation is that there is only one matching ebuild, and 226*760c253cSXin Li an assert is raised if this is not the case. However, symlinks to 227*760c253cSXin Li ebuilds in the same directory are ignored, so having a 228*760c253cSXin Li rust-x.y.z-rn.ebuild symlink to rust-x.y.z.ebuild is allowed. 229*760c253cSXin Li """ 230*760c253cSXin Li if version: 231*760c253cSXin Li pattern = f"{name}-{version}*.ebuild" 232*760c253cSXin Li else: 233*760c253cSXin Li pattern = f"{name}-*.ebuild" 234*760c253cSXin Li matches = set(directory.glob(pattern)) 235*760c253cSXin Li result = [] 236*760c253cSXin Li # Only count matches that are not links to other matches. 237*760c253cSXin Li for m in matches: 238*760c253cSXin Li try: 239*760c253cSXin Li target = os.readlink(directory / m) 240*760c253cSXin Li except OSError: 241*760c253cSXin Li # Getting here means the match is not a symlink to one of 242*760c253cSXin Li # the matching ebuilds, so add it to the result list. 243*760c253cSXin Li result.append(m) 244*760c253cSXin Li continue 245*760c253cSXin Li if directory / target not in matches: 246*760c253cSXin Li result.append(m) 247*760c253cSXin Li assert len(result) == 1, result 248*760c253cSXin Li return result[0] 249*760c253cSXin Li 250*760c253cSXin Li 251*760c253cSXin Lidef get_rust_bootstrap_version(): 252*760c253cSXin Li """Get the version of the current rust-bootstrap package.""" 253*760c253cSXin Li bootstrap_ebuild = find_ebuild_path(rust_bootstrap_path(), "rust-bootstrap") 254*760c253cSXin Li m = re.match(r"^rust-bootstrap-(\d+).(\d+).(\d+)", bootstrap_ebuild.name) 255*760c253cSXin Li assert m, bootstrap_ebuild.name 256*760c253cSXin Li return RustVersion(int(m.group(1)), int(m.group(2)), int(m.group(3))) 257*760c253cSXin Li 258*760c253cSXin Li 259*760c253cSXin Lidef parse_commandline_args() -> argparse.Namespace: 260*760c253cSXin Li parser = argparse.ArgumentParser( 261*760c253cSXin Li description=__doc__, 262*760c253cSXin Li formatter_class=argparse.RawDescriptionHelpFormatter, 263*760c253cSXin Li ) 264*760c253cSXin Li parser.add_argument( 265*760c253cSXin Li "--state_file", 266*760c253cSXin Li required=True, 267*760c253cSXin Li help="A state file to hold previous completed steps. If the file " 268*760c253cSXin Li "exists, it needs to be used together with --continue or --restart. " 269*760c253cSXin Li "If not exist (do not use --continue in this case), we will create a " 270*760c253cSXin Li "file for you.", 271*760c253cSXin Li ) 272*760c253cSXin Li parser.add_argument( 273*760c253cSXin Li "--restart", 274*760c253cSXin Li action="store_true", 275*760c253cSXin Li help="Restart from the first step. Ignore the completed steps in " 276*760c253cSXin Li "the state file", 277*760c253cSXin Li ) 278*760c253cSXin Li parser.add_argument( 279*760c253cSXin Li "--continue", 280*760c253cSXin Li dest="cont", 281*760c253cSXin Li action="store_true", 282*760c253cSXin Li help="Continue the steps from the state file", 283*760c253cSXin Li ) 284*760c253cSXin Li 285*760c253cSXin Li create_parser_template = argparse.ArgumentParser(add_help=False) 286*760c253cSXin Li create_parser_template.add_argument( 287*760c253cSXin Li "--template", 288*760c253cSXin Li type=RustVersion.parse, 289*760c253cSXin Li default=None, 290*760c253cSXin Li help="A template to use for creating a Rust uprev from, in the form " 291*760c253cSXin Li "a.b.c The ebuild has to exist in the chroot. If not specified, the " 292*760c253cSXin Li "tool will use the current Rust version in the chroot as template.", 293*760c253cSXin Li ) 294*760c253cSXin Li create_parser_template.add_argument( 295*760c253cSXin Li "--skip_compile", 296*760c253cSXin Li action="store_true", 297*760c253cSXin Li help="Skip compiling rust to test the tool. Only for testing", 298*760c253cSXin Li ) 299*760c253cSXin Li 300*760c253cSXin Li subparsers = parser.add_subparsers(dest="subparser_name") 301*760c253cSXin Li subparser_names = [] 302*760c253cSXin Li subparser_names.append("create") 303*760c253cSXin Li create_parser = subparsers.add_parser( 304*760c253cSXin Li "create", 305*760c253cSXin Li parents=[create_parser_template], 306*760c253cSXin Li help="Create changes uprevs Rust to a new version", 307*760c253cSXin Li ) 308*760c253cSXin Li create_parser.add_argument( 309*760c253cSXin Li "--rust_version", 310*760c253cSXin Li type=RustVersion.parse, 311*760c253cSXin Li required=True, 312*760c253cSXin Li help="Rust version to uprev to, in the form a.b.c", 313*760c253cSXin Li ) 314*760c253cSXin Li 315*760c253cSXin Li subparser_names.append("remove") 316*760c253cSXin Li remove_parser = subparsers.add_parser( 317*760c253cSXin Li "remove", 318*760c253cSXin Li help="Clean up old Rust version from chroot", 319*760c253cSXin Li ) 320*760c253cSXin Li remove_parser.add_argument( 321*760c253cSXin Li "--rust_version", 322*760c253cSXin Li type=RustVersion.parse, 323*760c253cSXin Li default=None, 324*760c253cSXin Li help="Rust version to remove, in the form a.b.c If not " 325*760c253cSXin Li "specified, the tool will remove the oldest version in the chroot", 326*760c253cSXin Li ) 327*760c253cSXin Li 328*760c253cSXin Li subparser_names.append("roll") 329*760c253cSXin Li roll_parser = subparsers.add_parser( 330*760c253cSXin Li "roll", 331*760c253cSXin Li parents=[create_parser_template], 332*760c253cSXin Li help="A command can create and upload a Rust uprev CL, including " 333*760c253cSXin Li "preparing the repo, creating new Rust uprev, deleting old uprev, " 334*760c253cSXin Li "and upload a CL to crrev.", 335*760c253cSXin Li ) 336*760c253cSXin Li roll_parser.add_argument( 337*760c253cSXin Li "--uprev", 338*760c253cSXin Li type=RustVersion.parse, 339*760c253cSXin Li required=True, 340*760c253cSXin Li help="Rust version to uprev to, in the form a.b.c", 341*760c253cSXin Li ) 342*760c253cSXin Li roll_parser.add_argument( 343*760c253cSXin Li "--remove", 344*760c253cSXin Li type=RustVersion.parse, 345*760c253cSXin Li default=None, 346*760c253cSXin Li help="Rust version to remove, in the form a.b.c If not " 347*760c253cSXin Li "specified, the tool will remove the oldest version in the chroot", 348*760c253cSXin Li ) 349*760c253cSXin Li roll_parser.add_argument( 350*760c253cSXin Li "--skip_cross_compiler", 351*760c253cSXin Li action="store_true", 352*760c253cSXin Li help="Skip updating cross-compiler in the chroot", 353*760c253cSXin Li ) 354*760c253cSXin Li roll_parser.add_argument( 355*760c253cSXin Li "--no_upload", 356*760c253cSXin Li action="store_true", 357*760c253cSXin Li help="If specified, the tool will not upload the CL for review", 358*760c253cSXin Li ) 359*760c253cSXin Li 360*760c253cSXin Li args = parser.parse_args() 361*760c253cSXin Li if args.subparser_name not in subparser_names: 362*760c253cSXin Li parser.error("one of %s must be specified" % subparser_names) 363*760c253cSXin Li 364*760c253cSXin Li if args.cont and args.restart: 365*760c253cSXin Li parser.error("Please select either --continue or --restart") 366*760c253cSXin Li 367*760c253cSXin Li if os.path.exists(args.state_file): 368*760c253cSXin Li if not args.cont and not args.restart: 369*760c253cSXin Li parser.error( 370*760c253cSXin Li "State file exists, so you should either --continue " 371*760c253cSXin Li "or --restart" 372*760c253cSXin Li ) 373*760c253cSXin Li if args.cont and not os.path.exists(args.state_file): 374*760c253cSXin Li parser.error("Indicate --continue but the state file does not exist") 375*760c253cSXin Li 376*760c253cSXin Li if args.restart and os.path.exists(args.state_file): 377*760c253cSXin Li os.remove(args.state_file) 378*760c253cSXin Li 379*760c253cSXin Li return args 380*760c253cSXin Li 381*760c253cSXin Li 382*760c253cSXin Lidef prepare_uprev( 383*760c253cSXin Li rust_version: RustVersion, template: RustVersion 384*760c253cSXin Li) -> Optional[PreparedUprev]: 385*760c253cSXin Li ebuild_path = find_ebuild_for_rust_version(template) 386*760c253cSXin Li 387*760c253cSXin Li if rust_version <= template: 388*760c253cSXin Li logging.info( 389*760c253cSXin Li "Requested version %s is not newer than the template version %s.", 390*760c253cSXin Li rust_version, 391*760c253cSXin Li template, 392*760c253cSXin Li ) 393*760c253cSXin Li return None 394*760c253cSXin Li 395*760c253cSXin Li logging.info( 396*760c253cSXin Li "Template Rust version is %s (ebuild: %s)", 397*760c253cSXin Li template, 398*760c253cSXin Li ebuild_path, 399*760c253cSXin Li ) 400*760c253cSXin Li 401*760c253cSXin Li return PreparedUprev(template) 402*760c253cSXin Li 403*760c253cSXin Li 404*760c253cSXin Lidef create_ebuild( 405*760c253cSXin Li category: str, 406*760c253cSXin Li name: str, 407*760c253cSXin Li template_version: RustVersion, 408*760c253cSXin Li new_version: RustVersion, 409*760c253cSXin Li) -> None: 410*760c253cSXin Li template_ebuild = compute_ebuild_path(category, name, template_version) 411*760c253cSXin Li new_ebuild = compute_ebuild_path(category, name, new_version) 412*760c253cSXin Li shutil.copyfile(template_ebuild, new_ebuild) 413*760c253cSXin Li subprocess.check_call( 414*760c253cSXin Li ["git", "add", new_ebuild.name], cwd=new_ebuild.parent 415*760c253cSXin Li ) 416*760c253cSXin Li 417*760c253cSXin Li 418*760c253cSXin Lidef set_include_profdata_src(ebuild_path: os.PathLike, include: bool) -> None: 419*760c253cSXin Li """Changes an ebuild file to include or omit profile data from SRC_URI. 420*760c253cSXin Li 421*760c253cSXin Li If include is True, the ebuild file will be rewritten to include 422*760c253cSXin Li profile data in SRC_URI. 423*760c253cSXin Li 424*760c253cSXin Li If include is False, the ebuild file will be rewritten to omit profile 425*760c253cSXin Li data from SRC_URI. 426*760c253cSXin Li """ 427*760c253cSXin Li if include: 428*760c253cSXin Li old = "" 429*760c253cSXin Li new = "yes" 430*760c253cSXin Li else: 431*760c253cSXin Li old = "yes" 432*760c253cSXin Li new = "" 433*760c253cSXin Li contents = Path(ebuild_path).read_text(encoding="utf-8") 434*760c253cSXin Li contents, subs = re.subn( 435*760c253cSXin Li f"^INCLUDE_PROFDATA_IN_SRC_URI={old}$", 436*760c253cSXin Li f"INCLUDE_PROFDATA_IN_SRC_URI={new}", 437*760c253cSXin Li contents, 438*760c253cSXin Li flags=re.MULTILINE, 439*760c253cSXin Li ) 440*760c253cSXin Li # We expect exactly one substitution. 441*760c253cSXin Li assert subs == 1, "Failed to update INCLUDE_PROFDATA_IN_SRC_URI" 442*760c253cSXin Li Path(ebuild_path).write_text(contents, encoding="utf-8") 443*760c253cSXin Li 444*760c253cSXin Li 445*760c253cSXin Lidef update_bootstrap_version( 446*760c253cSXin Li path: PathOrStr, new_bootstrap_version: RustVersion 447*760c253cSXin Li) -> None: 448*760c253cSXin Li path = Path(path) 449*760c253cSXin Li contents = path.read_text(encoding="utf-8") 450*760c253cSXin Li contents, subs = re.subn( 451*760c253cSXin Li r"^BOOTSTRAP_VERSION=.*$", 452*760c253cSXin Li 'BOOTSTRAP_VERSION="%s"' % (new_bootstrap_version,), 453*760c253cSXin Li contents, 454*760c253cSXin Li flags=re.MULTILINE, 455*760c253cSXin Li ) 456*760c253cSXin Li if not subs: 457*760c253cSXin Li raise RuntimeError(f"BOOTSTRAP_VERSION not found in {path}") 458*760c253cSXin Li path.write_text(contents, encoding="utf-8") 459*760c253cSXin Li logging.info("Rust BOOTSTRAP_VERSION updated to %s", new_bootstrap_version) 460*760c253cSXin Li 461*760c253cSXin Li 462*760c253cSXin Lidef ebuild_actions( 463*760c253cSXin Li package: str, actions: List[str], sudo: bool = False 464*760c253cSXin Li) -> None: 465*760c253cSXin Li ebuild_path_inchroot = find_ebuild_for_package(package) 466*760c253cSXin Li cmd = ["ebuild", ebuild_path_inchroot] + actions 467*760c253cSXin Li if sudo: 468*760c253cSXin Li cmd = ["sudo"] + cmd 469*760c253cSXin Li run_in_chroot(cmd) 470*760c253cSXin Li 471*760c253cSXin Li 472*760c253cSXin Lidef fetch_distfile_from_mirror(name: str) -> None: 473*760c253cSXin Li """Gets the named file from the local mirror. 474*760c253cSXin Li 475*760c253cSXin Li This ensures that the file exists on the mirror, and 476*760c253cSXin Li that we can read it. We overwrite any existing distfile 477*760c253cSXin Li to ensure the checksums that `ebuild manifest` records 478*760c253cSXin Li match the file as it exists on the mirror. 479*760c253cSXin Li 480*760c253cSXin Li This function also attempts to verify the ACL for 481*760c253cSXin Li the file (which is expected to have READER permission 482*760c253cSXin Li for allUsers). We can only see the ACL if the user 483*760c253cSXin Li gsutil runs with is the owner of the file. If not, 484*760c253cSXin Li we get an access denied error. We also count this 485*760c253cSXin Li as a success, because it means we were able to fetch 486*760c253cSXin Li the file even though we don't own it. 487*760c253cSXin Li """ 488*760c253cSXin Li mirror_file = MIRROR_PATH + "/" + name 489*760c253cSXin Li local_file = get_distdir() / name 490*760c253cSXin Li cmd: Command = [GSUTIL, "cp", mirror_file, local_file] 491*760c253cSXin Li logging.info("Running %r", cmd) 492*760c253cSXin Li rc = subprocess.call(cmd) 493*760c253cSXin Li if rc != 0: 494*760c253cSXin Li logging.error( 495*760c253cSXin Li """Could not fetch %s 496*760c253cSXin Li 497*760c253cSXin LiIf the file does not yet exist at %s 498*760c253cSXin Liplease download the file, verify its integrity 499*760c253cSXin Liwith something like: 500*760c253cSXin Li 501*760c253cSXin Licurl -O https://static.rust-lang.org/dist/%s 502*760c253cSXin Ligpg --verify %s.asc 503*760c253cSXin Li 504*760c253cSXin LiYou may need to import the signing key first, e.g.: 505*760c253cSXin Li 506*760c253cSXin Ligpg --recv-keys 85AB96E6FA1BE5FE 507*760c253cSXin Li 508*760c253cSXin LiOnce you have verify the integrity of the file, upload 509*760c253cSXin Liit to the local mirror using gsutil cp. 510*760c253cSXin Li""", 511*760c253cSXin Li mirror_file, 512*760c253cSXin Li MIRROR_PATH, 513*760c253cSXin Li name, 514*760c253cSXin Li name, 515*760c253cSXin Li ) 516*760c253cSXin Li raise Exception(f"Could not fetch {mirror_file}") 517*760c253cSXin Li # Check that the ACL allows allUsers READER access. 518*760c253cSXin Li # If we get an AccessDeniedAcception here, that also 519*760c253cSXin Li # counts as a success, because we were able to fetch 520*760c253cSXin Li # the file as a non-owner. 521*760c253cSXin Li cmd = [GSUTIL, "acl", "get", mirror_file] 522*760c253cSXin Li logging.info("Running %r", cmd) 523*760c253cSXin Li output = get_command_output_unchecked(cmd, stderr=subprocess.STDOUT) 524*760c253cSXin Li acl_verified = False 525*760c253cSXin Li if "AccessDeniedException:" in output: 526*760c253cSXin Li acl_verified = True 527*760c253cSXin Li else: 528*760c253cSXin Li acl = json.loads(output) 529*760c253cSXin Li for x in acl: 530*760c253cSXin Li if x["entity"] == "allUsers" and x["role"] == "READER": 531*760c253cSXin Li acl_verified = True 532*760c253cSXin Li break 533*760c253cSXin Li if not acl_verified: 534*760c253cSXin Li logging.error("Output from acl get:\n%s", output) 535*760c253cSXin Li raise Exception("Could not verify that allUsers has READER permission") 536*760c253cSXin Li 537*760c253cSXin Li 538*760c253cSXin Lidef fetch_bootstrap_distfiles(version: RustVersion) -> None: 539*760c253cSXin Li """Fetches rust-bootstrap distfiles from the local mirror 540*760c253cSXin Li 541*760c253cSXin Li Fetches the distfiles for a rust-bootstrap ebuild to ensure they 542*760c253cSXin Li are available on the mirror and the local copies are the same as 543*760c253cSXin Li the ones on the mirror. 544*760c253cSXin Li """ 545*760c253cSXin Li fetch_distfile_from_mirror(compute_rustc_src_name(version)) 546*760c253cSXin Li 547*760c253cSXin Li 548*760c253cSXin Lidef fetch_rust_distfiles(version: RustVersion) -> None: 549*760c253cSXin Li """Fetches rust distfiles from the local mirror 550*760c253cSXin Li 551*760c253cSXin Li Fetches the distfiles for a rust ebuild to ensure they 552*760c253cSXin Li are available on the mirror and the local copies are 553*760c253cSXin Li the same as the ones on the mirror. 554*760c253cSXin Li """ 555*760c253cSXin Li fetch_distfile_from_mirror(compute_rustc_src_name(version)) 556*760c253cSXin Li 557*760c253cSXin Li 558*760c253cSXin Lidef fetch_rust_src_from_upstream(uri: str, local_path: Path) -> None: 559*760c253cSXin Li """Fetches Rust sources from upstream. 560*760c253cSXin Li 561*760c253cSXin Li This downloads the source distribution and the .asc file 562*760c253cSXin Li containing the signatures. It then verifies that the sources 563*760c253cSXin Li have the expected signature and have been signed by 564*760c253cSXin Li the expected key. 565*760c253cSXin Li """ 566*760c253cSXin Li subprocess.run( 567*760c253cSXin Li [GPG, "--keyserver", GPG_KEYSERVER, "--recv-keys", RUST_SIGNING_KEY], 568*760c253cSXin Li check=True, 569*760c253cSXin Li ) 570*760c253cSXin Li subprocess.run( 571*760c253cSXin Li [GPG, "--keyserver", GPG_KEYSERVER, "--refresh-keys", RUST_SIGNING_KEY], 572*760c253cSXin Li check=True, 573*760c253cSXin Li ) 574*760c253cSXin Li asc_uri = uri + ".asc" 575*760c253cSXin Li local_asc_path = Path(local_path.parent, local_path.name + ".asc") 576*760c253cSXin Li logging.info("Fetching %s", uri) 577*760c253cSXin Li urllib.request.urlretrieve(uri, local_path) 578*760c253cSXin Li logging.info("%s fetched", uri) 579*760c253cSXin Li 580*760c253cSXin Li # Raise SignatureVerificationError if we cannot get the signature. 581*760c253cSXin Li try: 582*760c253cSXin Li logging.info("Fetching %s", asc_uri) 583*760c253cSXin Li urllib.request.urlretrieve(asc_uri, local_asc_path) 584*760c253cSXin Li logging.info("%s fetched", asc_uri) 585*760c253cSXin Li except Exception as e: 586*760c253cSXin Li raise SignatureVerificationError( 587*760c253cSXin Li f"error fetching signature file {asc_uri}", 588*760c253cSXin Li local_path, 589*760c253cSXin Li ) from e 590*760c253cSXin Li 591*760c253cSXin Li # Raise SignatureVerificationError if verifying the signature 592*760c253cSXin Li # failed. 593*760c253cSXin Li try: 594*760c253cSXin Li output = get_command_output( 595*760c253cSXin Li [GPG, "--verify", "--status-fd", "1", local_asc_path] 596*760c253cSXin Li ) 597*760c253cSXin Li except subprocess.CalledProcessError as e: 598*760c253cSXin Li raise SignatureVerificationError( 599*760c253cSXin Li f"error verifying signature. GPG output:\n{e.stdout}", 600*760c253cSXin Li local_path, 601*760c253cSXin Li ) from e 602*760c253cSXin Li 603*760c253cSXin Li # Raise SignatureVerificationError if the file was not signed 604*760c253cSXin Li # with the expected key. 605*760c253cSXin Li if f"GOODSIG {RUST_SIGNING_KEY}" not in output: 606*760c253cSXin Li message = f"GOODSIG {RUST_SIGNING_KEY} not found in output" 607*760c253cSXin Li if f"REVKEYSIG {RUST_SIGNING_KEY}" in output: 608*760c253cSXin Li message = "signing key has been revoked" 609*760c253cSXin Li elif f"EXPKEYSIG {RUST_SIGNING_KEY}" in output: 610*760c253cSXin Li message = "signing key has expired" 611*760c253cSXin Li elif f"EXPSIG {RUST_SIGNING_KEY}" in output: 612*760c253cSXin Li message = "signature has expired" 613*760c253cSXin Li raise SignatureVerificationError( 614*760c253cSXin Li f"{message}. GPG output:\n{output}", 615*760c253cSXin Li local_path, 616*760c253cSXin Li ) 617*760c253cSXin Li 618*760c253cSXin Li 619*760c253cSXin Lidef get_distdir() -> Path: 620*760c253cSXin Li """Returns portage's distdir outside the chroot.""" 621*760c253cSXin Li return SOURCE_ROOT / ".cache/distfiles" 622*760c253cSXin Li 623*760c253cSXin Li 624*760c253cSXin Lidef mirror_has_file(name: str) -> bool: 625*760c253cSXin Li """Checks if the mirror has the named file.""" 626*760c253cSXin Li mirror_file = MIRROR_PATH + "/" + name 627*760c253cSXin Li cmd: Command = [GSUTIL, "ls", mirror_file] 628*760c253cSXin Li proc = subprocess.run( 629*760c253cSXin Li cmd, 630*760c253cSXin Li check=False, 631*760c253cSXin Li stdout=subprocess.PIPE, 632*760c253cSXin Li stderr=subprocess.STDOUT, 633*760c253cSXin Li encoding="utf-8", 634*760c253cSXin Li ) 635*760c253cSXin Li if "URLs matched no objects" in proc.stdout: 636*760c253cSXin Li return False 637*760c253cSXin Li elif proc.returncode == 0: 638*760c253cSXin Li return True 639*760c253cSXin Li 640*760c253cSXin Li raise Exception( 641*760c253cSXin Li "Unexpected result from gsutil ls:" 642*760c253cSXin Li f" rc {proc.returncode} output:\n{proc.stdout}" 643*760c253cSXin Li ) 644*760c253cSXin Li 645*760c253cSXin Li 646*760c253cSXin Lidef mirror_rust_source(version: RustVersion) -> None: 647*760c253cSXin Li """Ensures source code for a Rust version is on the local mirror. 648*760c253cSXin Li 649*760c253cSXin Li If the source code is not found on the mirror, it is fetched 650*760c253cSXin Li from upstream, its integrity is verified, and it is uploaded 651*760c253cSXin Li to the mirror. 652*760c253cSXin Li """ 653*760c253cSXin Li filename = compute_rustc_src_name(version) 654*760c253cSXin Li if mirror_has_file(filename): 655*760c253cSXin Li logging.info("%s is present on the mirror", filename) 656*760c253cSXin Li return 657*760c253cSXin Li uri = f"{RUST_SRC_BASE_URI}{filename}" 658*760c253cSXin Li local_path = get_distdir() / filename 659*760c253cSXin Li mirror_path = f"{MIRROR_PATH}/{filename}" 660*760c253cSXin Li fetch_rust_src_from_upstream(uri, local_path) 661*760c253cSXin Li subprocess.run( 662*760c253cSXin Li [GSUTIL, "cp", "-a", "public-read", local_path, mirror_path], 663*760c253cSXin Li check=True, 664*760c253cSXin Li ) 665*760c253cSXin Li 666*760c253cSXin Li 667*760c253cSXin Lidef update_rust_packages( 668*760c253cSXin Li pkgatom: str, rust_version: RustVersion, add: bool 669*760c253cSXin Li) -> None: 670*760c253cSXin Li package_file = EBUILD_PREFIX.joinpath( 671*760c253cSXin Li "profiles/targets/chromeos/package.provided" 672*760c253cSXin Li ) 673*760c253cSXin Li with open(package_file, encoding="utf-8") as f: 674*760c253cSXin Li contents = f.read() 675*760c253cSXin Li if add: 676*760c253cSXin Li rust_packages_re = re.compile( 677*760c253cSXin Li "^" + re.escape(pkgatom) + r"-\d+\.\d+\.\d+$", re.MULTILINE 678*760c253cSXin Li ) 679*760c253cSXin Li rust_packages = rust_packages_re.findall(contents) 680*760c253cSXin Li # Assume all the rust packages are in alphabetical order, so insert 681*760c253cSXin Li # the new version to the place after the last rust_packages 682*760c253cSXin Li new_str = f"{pkgatom}-{rust_version}" 683*760c253cSXin Li new_contents = contents.replace( 684*760c253cSXin Li rust_packages[-1], f"{rust_packages[-1]}\n{new_str}" 685*760c253cSXin Li ) 686*760c253cSXin Li logging.info("%s has been inserted into package.provided", new_str) 687*760c253cSXin Li else: 688*760c253cSXin Li old_str = f"{pkgatom}-{rust_version}\n" 689*760c253cSXin Li assert old_str in contents, f"{old_str!r} not found in package.provided" 690*760c253cSXin Li new_contents = contents.replace(old_str, "") 691*760c253cSXin Li logging.info("%s has been removed from package.provided", old_str) 692*760c253cSXin Li 693*760c253cSXin Li with open(package_file, "w", encoding="utf-8") as f: 694*760c253cSXin Li f.write(new_contents) 695*760c253cSXin Li 696*760c253cSXin Li 697*760c253cSXin Lidef update_virtual_rust( 698*760c253cSXin Li template_version: RustVersion, new_version: RustVersion 699*760c253cSXin Li) -> None: 700*760c253cSXin Li template_ebuild = find_ebuild_path( 701*760c253cSXin Li EBUILD_PREFIX.joinpath("virtual/rust"), "rust", template_version 702*760c253cSXin Li ) 703*760c253cSXin Li virtual_rust_dir = template_ebuild.parent 704*760c253cSXin Li new_name = f"rust-{new_version}.ebuild" 705*760c253cSXin Li new_ebuild = virtual_rust_dir.joinpath(new_name) 706*760c253cSXin Li shutil.copyfile(template_ebuild, new_ebuild) 707*760c253cSXin Li subprocess.check_call(["git", "add", new_name], cwd=virtual_rust_dir) 708*760c253cSXin Li 709*760c253cSXin Li 710*760c253cSXin Lidef unmerge_package_if_installed(pkgatom: str) -> None: 711*760c253cSXin Li """Unmerges a package if it is installed.""" 712*760c253cSXin Li shpkg = shlex.quote(pkgatom) 713*760c253cSXin Li run_in_chroot( 714*760c253cSXin Li [ 715*760c253cSXin Li "sudo", 716*760c253cSXin Li "bash", 717*760c253cSXin Li "-c", 718*760c253cSXin Li f"! emerge --pretend --quiet --unmerge {shpkg}" 719*760c253cSXin Li f" || emerge --rage-clean {shpkg}", 720*760c253cSXin Li ], 721*760c253cSXin Li ) 722*760c253cSXin Li 723*760c253cSXin Li 724*760c253cSXin Lidef perform_step( 725*760c253cSXin Li state_file: pathlib.Path, 726*760c253cSXin Li tmp_state_file: pathlib.Path, 727*760c253cSXin Li completed_steps: Dict[str, Any], 728*760c253cSXin Li step_name: str, 729*760c253cSXin Li step_fn: Callable[[], T], 730*760c253cSXin Li result_from_json: Optional[Callable[[Any], T]] = None, 731*760c253cSXin Li result_to_json: Optional[Callable[[T], Any]] = None, 732*760c253cSXin Li) -> T: 733*760c253cSXin Li if step_name in completed_steps: 734*760c253cSXin Li logging.info("Skipping previously completed step %s", step_name) 735*760c253cSXin Li if result_from_json: 736*760c253cSXin Li return result_from_json(completed_steps[step_name]) 737*760c253cSXin Li return completed_steps[step_name] 738*760c253cSXin Li 739*760c253cSXin Li logging.info("Running step %s", step_name) 740*760c253cSXin Li val = step_fn() 741*760c253cSXin Li logging.info("Step %s complete", step_name) 742*760c253cSXin Li if result_to_json: 743*760c253cSXin Li completed_steps[step_name] = result_to_json(val) 744*760c253cSXin Li else: 745*760c253cSXin Li completed_steps[step_name] = val 746*760c253cSXin Li 747*760c253cSXin Li with tmp_state_file.open("w", encoding="utf-8") as f: 748*760c253cSXin Li json.dump(completed_steps, f, indent=4) 749*760c253cSXin Li tmp_state_file.rename(state_file) 750*760c253cSXin Li return val 751*760c253cSXin Li 752*760c253cSXin Li 753*760c253cSXin Lidef prepare_uprev_from_json(obj: Any) -> Optional[PreparedUprev]: 754*760c253cSXin Li if not obj: 755*760c253cSXin Li return None 756*760c253cSXin Li version = obj[0] 757*760c253cSXin Li return PreparedUprev( 758*760c253cSXin Li RustVersion(*version), 759*760c253cSXin Li ) 760*760c253cSXin Li 761*760c253cSXin Li 762*760c253cSXin Lidef prepare_uprev_to_json( 763*760c253cSXin Li prepared_uprev: Optional[PreparedUprev], 764*760c253cSXin Li) -> Optional[Tuple[RustVersion]]: 765*760c253cSXin Li if prepared_uprev is None: 766*760c253cSXin Li return None 767*760c253cSXin Li return (prepared_uprev.template_version,) 768*760c253cSXin Li 769*760c253cSXin Li 770*760c253cSXin Lidef create_rust_uprev( 771*760c253cSXin Li rust_version: RustVersion, 772*760c253cSXin Li template_version: RustVersion, 773*760c253cSXin Li skip_compile: bool, 774*760c253cSXin Li run_step: RunStepFn, 775*760c253cSXin Li) -> None: 776*760c253cSXin Li prepared = run_step( 777*760c253cSXin Li "prepare uprev", 778*760c253cSXin Li lambda: prepare_uprev(rust_version, template_version), 779*760c253cSXin Li result_from_json=prepare_uprev_from_json, 780*760c253cSXin Li result_to_json=prepare_uprev_to_json, 781*760c253cSXin Li ) 782*760c253cSXin Li if prepared is None: 783*760c253cSXin Li return 784*760c253cSXin Li template_version = prepared.template_version 785*760c253cSXin Li 786*760c253cSXin Li run_step( 787*760c253cSXin Li "mirror bootstrap sources", 788*760c253cSXin Li lambda: mirror_rust_source( 789*760c253cSXin Li template_version, 790*760c253cSXin Li ), 791*760c253cSXin Li ) 792*760c253cSXin Li run_step( 793*760c253cSXin Li "mirror rust sources", 794*760c253cSXin Li lambda: mirror_rust_source( 795*760c253cSXin Li rust_version, 796*760c253cSXin Li ), 797*760c253cSXin Li ) 798*760c253cSXin Li 799*760c253cSXin Li # The fetch steps will fail (on purpose) if the files they check for 800*760c253cSXin Li # are not available on the mirror. To make them pass, fetch the 801*760c253cSXin Li # required files yourself, verify their checksums, then upload them 802*760c253cSXin Li # to the mirror. 803*760c253cSXin Li run_step( 804*760c253cSXin Li "fetch bootstrap distfiles", 805*760c253cSXin Li lambda: fetch_bootstrap_distfiles(template_version), 806*760c253cSXin Li ) 807*760c253cSXin Li run_step("fetch rust distfiles", lambda: fetch_rust_distfiles(rust_version)) 808*760c253cSXin Li run_step( 809*760c253cSXin Li "update bootstrap version", 810*760c253cSXin Li lambda: update_bootstrap_version(CROS_RUSTC_ECLASS, template_version), 811*760c253cSXin Li ) 812*760c253cSXin Li run_step( 813*760c253cSXin Li "turn off profile data sources in cros-rustc.eclass", 814*760c253cSXin Li lambda: set_include_profdata_src(CROS_RUSTC_ECLASS, include=False), 815*760c253cSXin Li ) 816*760c253cSXin Li 817*760c253cSXin Li for category, name in RUST_PACKAGES: 818*760c253cSXin Li run_step( 819*760c253cSXin Li f"create new {category}/{name} ebuild", 820*760c253cSXin Li functools.partial( 821*760c253cSXin Li create_ebuild, 822*760c253cSXin Li category, 823*760c253cSXin Li name, 824*760c253cSXin Li template_version, 825*760c253cSXin Li rust_version, 826*760c253cSXin Li ), 827*760c253cSXin Li ) 828*760c253cSXin Li 829*760c253cSXin Li run_step( 830*760c253cSXin Li "update dev-lang/rust-host manifest to add new version", 831*760c253cSXin Li lambda: ebuild_actions("dev-lang/rust-host", ["manifest"]), 832*760c253cSXin Li ) 833*760c253cSXin Li 834*760c253cSXin Li run_step( 835*760c253cSXin Li "generate profile data for rustc", 836*760c253cSXin Li lambda: run_in_chroot([PGO_RUST, "generate"]), 837*760c253cSXin Li # Avoid returning subprocess.CompletedProcess, which cannot be 838*760c253cSXin Li # serialized to JSON. 839*760c253cSXin Li result_to_json=lambda _x: None, 840*760c253cSXin Li ) 841*760c253cSXin Li run_step( 842*760c253cSXin Li "upload profile data for rustc", 843*760c253cSXin Li lambda: run_in_chroot([PGO_RUST, "upload-profdata"]), 844*760c253cSXin Li # Avoid returning subprocess.CompletedProcess, which cannot be 845*760c253cSXin Li # serialized to JSON. 846*760c253cSXin Li result_to_json=lambda _x: None, 847*760c253cSXin Li ) 848*760c253cSXin Li run_step( 849*760c253cSXin Li "turn on profile data sources in cros-rustc.eclass", 850*760c253cSXin Li lambda: set_include_profdata_src(CROS_RUSTC_ECLASS, include=True), 851*760c253cSXin Li ) 852*760c253cSXin Li run_step( 853*760c253cSXin Li "update dev-lang/rust-host manifest to add profile data", 854*760c253cSXin Li lambda: ebuild_actions("dev-lang/rust-host", ["manifest"]), 855*760c253cSXin Li ) 856*760c253cSXin Li if not skip_compile: 857*760c253cSXin Li run_step("build packages", lambda: rebuild_packages(rust_version)) 858*760c253cSXin Li run_step( 859*760c253cSXin Li "insert host version into rust packages", 860*760c253cSXin Li lambda: update_rust_packages( 861*760c253cSXin Li "dev-lang/rust-host", rust_version, add=True 862*760c253cSXin Li ), 863*760c253cSXin Li ) 864*760c253cSXin Li run_step( 865*760c253cSXin Li "insert target version into rust packages", 866*760c253cSXin Li lambda: update_rust_packages("dev-lang/rust", rust_version, add=True), 867*760c253cSXin Li ) 868*760c253cSXin Li run_step( 869*760c253cSXin Li "upgrade virtual/rust", 870*760c253cSXin Li lambda: update_virtual_rust(template_version, rust_version), 871*760c253cSXin Li ) 872*760c253cSXin Li 873*760c253cSXin Li 874*760c253cSXin Lidef find_rust_versions() -> List[Tuple[RustVersion, Path]]: 875*760c253cSXin Li """Returns (RustVersion, ebuild_path) for base versions of dev-lang/rust. 876*760c253cSXin Li 877*760c253cSXin Li This excludes symlinks to ebuilds, so if rust-1.34.0.ebuild and 878*760c253cSXin Li rust-1.34.0-r1.ebuild both exist and -r1 is a symlink to the other, 879*760c253cSXin Li only rust-1.34.0.ebuild will be in the return value. 880*760c253cSXin Li """ 881*760c253cSXin Li return [ 882*760c253cSXin Li (RustVersion.parse_from_ebuild(ebuild), ebuild) 883*760c253cSXin Li for ebuild in RUST_PATH.iterdir() 884*760c253cSXin Li if ebuild.suffix == ".ebuild" and not ebuild.is_symlink() 885*760c253cSXin Li ] 886*760c253cSXin Li 887*760c253cSXin Li 888*760c253cSXin Lidef find_oldest_rust_version() -> RustVersion: 889*760c253cSXin Li """Returns the RustVersion of the oldest dev-lang/rust ebuild.""" 890*760c253cSXin Li rust_versions = find_rust_versions() 891*760c253cSXin Li if len(rust_versions) <= 1: 892*760c253cSXin Li raise RuntimeError("Expect to find more than one Rust versions") 893*760c253cSXin Li return min(rust_versions)[0] 894*760c253cSXin Li 895*760c253cSXin Li 896*760c253cSXin Lidef find_ebuild_for_rust_version(version: RustVersion) -> Path: 897*760c253cSXin Li """Returns the path of the ebuild for the given version of dev-lang/rust.""" 898*760c253cSXin Li return find_ebuild_path(RUST_PATH, "rust", version) 899*760c253cSXin Li 900*760c253cSXin Li 901*760c253cSXin Lidef rebuild_packages(version: RustVersion): 902*760c253cSXin Li """Rebuild packages modified by this script.""" 903*760c253cSXin Li # Remove all packages we modify to avoid depending on preinstalled 904*760c253cSXin Li # versions. This ensures that the packages can really be built. 905*760c253cSXin Li packages = [f"{category}/{name}" for category, name in RUST_PACKAGES] 906*760c253cSXin Li for pkg in packages: 907*760c253cSXin Li unmerge_package_if_installed(pkg) 908*760c253cSXin Li # Mention only dev-lang/rust explicitly, so that others are pulled 909*760c253cSXin Li # in as dependencies (letting us detect dependency errors). 910*760c253cSXin Li # Packages we modify are listed in --usepkg-exclude to ensure they 911*760c253cSXin Li # are built from source. 912*760c253cSXin Li try: 913*760c253cSXin Li run_in_chroot( 914*760c253cSXin Li [ 915*760c253cSXin Li "sudo", 916*760c253cSXin Li "emerge", 917*760c253cSXin Li "--quiet-build", 918*760c253cSXin Li "--usepkg-exclude", 919*760c253cSXin Li " ".join(packages), 920*760c253cSXin Li f"=dev-lang/rust-{version}", 921*760c253cSXin Li ], 922*760c253cSXin Li ) 923*760c253cSXin Li except: 924*760c253cSXin Li logging.warning( 925*760c253cSXin Li "Failed to build dev-lang/rust or one of its dependencies." 926*760c253cSXin Li " If necessary, you can restore rust and rust-host from" 927*760c253cSXin Li " binary packages:\n sudo emerge --getbinpkgonly dev-lang/rust" 928*760c253cSXin Li ) 929*760c253cSXin Li raise 930*760c253cSXin Li 931*760c253cSXin Li 932*760c253cSXin Lidef remove_ebuild_version(path: PathOrStr, name: str, version: RustVersion): 933*760c253cSXin Li """Remove the specified version of an ebuild. 934*760c253cSXin Li 935*760c253cSXin Li Removes {path}/{name}-{version}.ebuild and {path}/{name}-{version}-*.ebuild 936*760c253cSXin Li using git rm. 937*760c253cSXin Li 938*760c253cSXin Li Args: 939*760c253cSXin Li path: The directory in which the ebuild files are. 940*760c253cSXin Li name: The name of the package (e.g. 'rust'). 941*760c253cSXin Li version: The version of the ebuild to remove. 942*760c253cSXin Li """ 943*760c253cSXin Li path = Path(path) 944*760c253cSXin Li pattern = f"{name}-{version}-*.ebuild" 945*760c253cSXin Li matches = list(path.glob(pattern)) 946*760c253cSXin Li ebuild = path / f"{name}-{version}.ebuild" 947*760c253cSXin Li if ebuild.exists(): 948*760c253cSXin Li matches.append(ebuild) 949*760c253cSXin Li if not matches: 950*760c253cSXin Li logging.warning( 951*760c253cSXin Li "No ebuilds matching %s version %s in %r", name, version, str(path) 952*760c253cSXin Li ) 953*760c253cSXin Li for m in matches: 954*760c253cSXin Li remove_files(m.name, path) 955*760c253cSXin Li 956*760c253cSXin Li 957*760c253cSXin Lidef remove_files(filename: PathOrStr, path: PathOrStr) -> None: 958*760c253cSXin Li subprocess.check_call(["git", "rm", filename], cwd=path) 959*760c253cSXin Li 960*760c253cSXin Li 961*760c253cSXin Lidef remove_rust_uprev( 962*760c253cSXin Li rust_version: Optional[RustVersion], 963*760c253cSXin Li run_step: RunStepFn, 964*760c253cSXin Li) -> None: 965*760c253cSXin Li def find_desired_rust_version() -> RustVersion: 966*760c253cSXin Li if rust_version: 967*760c253cSXin Li return rust_version 968*760c253cSXin Li return find_oldest_rust_version() 969*760c253cSXin Li 970*760c253cSXin Li def find_desired_rust_version_from_json(obj: Any) -> RustVersion: 971*760c253cSXin Li return RustVersion(*obj) 972*760c253cSXin Li 973*760c253cSXin Li delete_version = run_step( 974*760c253cSXin Li "find rust version to delete", 975*760c253cSXin Li find_desired_rust_version, 976*760c253cSXin Li result_from_json=find_desired_rust_version_from_json, 977*760c253cSXin Li ) 978*760c253cSXin Li 979*760c253cSXin Li for category, name in RUST_PACKAGES: 980*760c253cSXin Li run_step( 981*760c253cSXin Li f"remove old {name} ebuild", 982*760c253cSXin Li functools.partial( 983*760c253cSXin Li remove_ebuild_version, 984*760c253cSXin Li EBUILD_PREFIX / category / name, 985*760c253cSXin Li name, 986*760c253cSXin Li delete_version, 987*760c253cSXin Li ), 988*760c253cSXin Li ) 989*760c253cSXin Li 990*760c253cSXin Li run_step( 991*760c253cSXin Li "update dev-lang/rust-host manifest to delete old version", 992*760c253cSXin Li lambda: ebuild_actions("dev-lang/rust-host", ["manifest"]), 993*760c253cSXin Li ) 994*760c253cSXin Li run_step( 995*760c253cSXin Li "remove target version from rust packages", 996*760c253cSXin Li lambda: update_rust_packages( 997*760c253cSXin Li "dev-lang/rust", delete_version, add=False 998*760c253cSXin Li ), 999*760c253cSXin Li ) 1000*760c253cSXin Li run_step( 1001*760c253cSXin Li "remove host version from rust packages", 1002*760c253cSXin Li lambda: update_rust_packages( 1003*760c253cSXin Li "dev-lang/rust-host", delete_version, add=False 1004*760c253cSXin Li ), 1005*760c253cSXin Li ) 1006*760c253cSXin Li run_step("remove virtual/rust", lambda: remove_virtual_rust(delete_version)) 1007*760c253cSXin Li 1008*760c253cSXin Li 1009*760c253cSXin Lidef remove_virtual_rust(delete_version: RustVersion) -> None: 1010*760c253cSXin Li remove_ebuild_version( 1011*760c253cSXin Li EBUILD_PREFIX.joinpath("virtual/rust"), "rust", delete_version 1012*760c253cSXin Li ) 1013*760c253cSXin Li 1014*760c253cSXin Li 1015*760c253cSXin Lidef rust_bootstrap_path() -> Path: 1016*760c253cSXin Li return EBUILD_PREFIX.joinpath("dev-lang/rust-bootstrap") 1017*760c253cSXin Li 1018*760c253cSXin Li 1019*760c253cSXin Lidef create_new_repo(rust_version: RustVersion) -> None: 1020*760c253cSXin Li output = get_command_output( 1021*760c253cSXin Li ["git", "status", "--porcelain"], cwd=EBUILD_PREFIX 1022*760c253cSXin Li ) 1023*760c253cSXin Li if output: 1024*760c253cSXin Li raise RuntimeError( 1025*760c253cSXin Li f"{EBUILD_PREFIX} has uncommitted changes, please either discard " 1026*760c253cSXin Li "them or commit them." 1027*760c253cSXin Li ) 1028*760c253cSXin Li git.CreateBranch(EBUILD_PREFIX, f"rust-to-{rust_version}") 1029*760c253cSXin Li 1030*760c253cSXin Li 1031*760c253cSXin Lidef build_cross_compiler(template_version: RustVersion) -> None: 1032*760c253cSXin Li # Get target triples in ebuild 1033*760c253cSXin Li rust_ebuild = find_ebuild_path(RUST_PATH, "rust", template_version) 1034*760c253cSXin Li contents = rust_ebuild.read_text(encoding="utf-8") 1035*760c253cSXin Li 1036*760c253cSXin Li target_triples_re = re.compile(r"RUSTC_TARGET_TRIPLES=\(([^)]+)\)") 1037*760c253cSXin Li m = target_triples_re.search(contents) 1038*760c253cSXin Li assert m, "RUST_TARGET_TRIPLES not found in rust ebuild" 1039*760c253cSXin Li target_triples = m.group(1).strip().split("\n") 1040*760c253cSXin Li 1041*760c253cSXin Li compiler_targets_to_install = [ 1042*760c253cSXin Li target.strip() for target in target_triples if "cros-" in target 1043*760c253cSXin Li ] 1044*760c253cSXin Li for target in target_triples: 1045*760c253cSXin Li if "cros-" not in target: 1046*760c253cSXin Li continue 1047*760c253cSXin Li target = target.strip() 1048*760c253cSXin Li 1049*760c253cSXin Li # We also always need arm-none-eabi, though it's not mentioned in 1050*760c253cSXin Li # RUSTC_TARGET_TRIPLES. 1051*760c253cSXin Li compiler_targets_to_install.append("arm-none-eabi") 1052*760c253cSXin Li 1053*760c253cSXin Li logging.info("Emerging cross compilers %s", compiler_targets_to_install) 1054*760c253cSXin Li run_in_chroot( 1055*760c253cSXin Li ["sudo", "emerge", "-j", "-G"] 1056*760c253cSXin Li + [f"cross-{target}/gcc" for target in compiler_targets_to_install], 1057*760c253cSXin Li ) 1058*760c253cSXin Li 1059*760c253cSXin Li 1060*760c253cSXin Lidef create_new_commit(rust_version: RustVersion) -> None: 1061*760c253cSXin Li subprocess.check_call(["git", "add", "-A"], cwd=EBUILD_PREFIX) 1062*760c253cSXin Li messages = [ 1063*760c253cSXin Li f"[DO NOT SUBMIT] dev-lang/rust: upgrade to Rust {rust_version}", 1064*760c253cSXin Li "", 1065*760c253cSXin Li "This CL is created by rust_uprev tool automatically." "", 1066*760c253cSXin Li "BUG=None", 1067*760c253cSXin Li "TEST=Use CQ to test the new Rust version", 1068*760c253cSXin Li ] 1069*760c253cSXin Li branch = f"rust-to-{rust_version}" 1070*760c253cSXin Li git.CommitChanges(EBUILD_PREFIX, messages) 1071*760c253cSXin Li git.UploadChanges(EBUILD_PREFIX, branch) 1072*760c253cSXin Li 1073*760c253cSXin Li 1074*760c253cSXin Lidef run_in_chroot(cmd: Command, *args, **kwargs) -> subprocess.CompletedProcess: 1075*760c253cSXin Li """Runs a command in the ChromiumOS chroot. 1076*760c253cSXin Li 1077*760c253cSXin Li This takes the same arguments as subprocess.run(). By default, 1078*760c253cSXin Li it uses check=True, encoding="utf-8". If needed, these can be 1079*760c253cSXin Li overridden by keyword arguments passed to run_in_chroot(). 1080*760c253cSXin Li """ 1081*760c253cSXin Li full_kwargs = dict( 1082*760c253cSXin Li { 1083*760c253cSXin Li "check": True, 1084*760c253cSXin Li "encoding": "utf-8", 1085*760c253cSXin Li }, 1086*760c253cSXin Li **kwargs, 1087*760c253cSXin Li ) 1088*760c253cSXin Li full_cmd = ["cros_sdk", "--"] + list(cmd) 1089*760c253cSXin Li logging.info("Running %s", shlex.join(str(x) for x in full_cmd)) 1090*760c253cSXin Li # pylint: disable=subprocess-run-check 1091*760c253cSXin Li # (check is actually set above; it defaults to True) 1092*760c253cSXin Li return subprocess.run(full_cmd, *args, **full_kwargs) 1093*760c253cSXin Li 1094*760c253cSXin Li 1095*760c253cSXin Lidef sudo_keepalive() -> None: 1096*760c253cSXin Li """Ensures we have sudo credentials, and keeps them up-to-date. 1097*760c253cSXin Li 1098*760c253cSXin Li Some operations (notably run_in_chroot) run sudo, which may require 1099*760c253cSXin Li user interaction. To avoid blocking progress while we sit waiting 1100*760c253cSXin Li for that interaction, sudo_keepalive checks that we have cached 1101*760c253cSXin Li sudo credentials, gets them if necessary, then keeps them up-to-date 1102*760c253cSXin Li so that the rest of the script can run without needing to get 1103*760c253cSXin Li sudo credentials again. 1104*760c253cSXin Li """ 1105*760c253cSXin Li logging.info( 1106*760c253cSXin Li "Caching sudo credentials for running commands inside the chroot" 1107*760c253cSXin Li ) 1108*760c253cSXin Li # Exits successfully if cached credentials exist. Otherwise, tries 1109*760c253cSXin Li # created cached credentials, prompting for authentication if necessary. 1110*760c253cSXin Li subprocess.run(["sudo", "true"], check=True) 1111*760c253cSXin Li 1112*760c253cSXin Li def sudo_keepalive_loop() -> None: 1113*760c253cSXin Li # Between credential refreshes, we sleep so that we don't 1114*760c253cSXin Li # unnecessarily burn CPU cycles. The sleep time must be shorter 1115*760c253cSXin Li # than sudo's configured cached credential expiration time, which 1116*760c253cSXin Li # is 15 minutes by default. 1117*760c253cSXin Li sleep_seconds = 10 * 60 1118*760c253cSXin Li # So as to not keep credentials cached forever, we limit the number 1119*760c253cSXin Li # of times we will refresh them. 1120*760c253cSXin Li max_seconds = 16 * 3600 1121*760c253cSXin Li max_refreshes = max_seconds // sleep_seconds 1122*760c253cSXin Li for _x in range(max_refreshes): 1123*760c253cSXin Li # Refreshes cached credentials if they exist, but never prompts 1124*760c253cSXin Li # for anything. If cached credentials do not exist, this 1125*760c253cSXin Li # command exits with an error. We ignore that error to keep the 1126*760c253cSXin Li # loop going, so that cached credentials will be kept fresh 1127*760c253cSXin Li # again once we have them (e.g. after the next cros_sdk command 1128*760c253cSXin Li # successfully authenticates the user). 1129*760c253cSXin Li # 1130*760c253cSXin Li # The standard file descriptors are all redirected to/from 1131*760c253cSXin Li # /dev/null to prevent this command from consuming any input 1132*760c253cSXin Li # or mixing its output with that of the other commands rust_uprev 1133*760c253cSXin Li # runs (which could be confusing, for example making it look like 1134*760c253cSXin Li # errors occurred during a build when they are actually in a 1135*760c253cSXin Li # separate task). 1136*760c253cSXin Li # 1137*760c253cSXin Li # Note: The command specifically uses "true" and not "-v", because 1138*760c253cSXin Li # it turns out that "-v" actually will prompt for a password when 1139*760c253cSXin Li # sudo is configured with NOPASSWD=all, even though in that case 1140*760c253cSXin Li # no password is required to run actual commands. 1141*760c253cSXin Li subprocess.run( 1142*760c253cSXin Li ["sudo", "-n", "true"], 1143*760c253cSXin Li check=False, 1144*760c253cSXin Li stdin=subprocess.DEVNULL, 1145*760c253cSXin Li stdout=subprocess.DEVNULL, 1146*760c253cSXin Li stderr=subprocess.DEVNULL, 1147*760c253cSXin Li ) 1148*760c253cSXin Li time.sleep(sleep_seconds) 1149*760c253cSXin Li 1150*760c253cSXin Li # daemon=True causes the thread to be killed when the script exits. 1151*760c253cSXin Li threading.Thread(target=sudo_keepalive_loop, daemon=True).start() 1152*760c253cSXin Li 1153*760c253cSXin Li 1154*760c253cSXin Lidef main() -> None: 1155*760c253cSXin Li chroot.VerifyOutsideChroot() 1156*760c253cSXin Li logging.basicConfig(level=logging.INFO) 1157*760c253cSXin Li args = parse_commandline_args() 1158*760c253cSXin Li state_file = pathlib.Path(args.state_file) 1159*760c253cSXin Li tmp_state_file = state_file.with_suffix(".tmp") 1160*760c253cSXin Li 1161*760c253cSXin Li try: 1162*760c253cSXin Li with state_file.open(encoding="utf-8") as f: 1163*760c253cSXin Li completed_steps = json.load(f) 1164*760c253cSXin Li except FileNotFoundError: 1165*760c253cSXin Li completed_steps = {} 1166*760c253cSXin Li 1167*760c253cSXin Li def run_step( 1168*760c253cSXin Li step_name: str, 1169*760c253cSXin Li step_fn: Callable[[], T], 1170*760c253cSXin Li result_from_json: Optional[Callable[[Any], T]] = None, 1171*760c253cSXin Li result_to_json: Optional[Callable[[T], Any]] = None, 1172*760c253cSXin Li ) -> T: 1173*760c253cSXin Li return perform_step( 1174*760c253cSXin Li state_file, 1175*760c253cSXin Li tmp_state_file, 1176*760c253cSXin Li completed_steps, 1177*760c253cSXin Li step_name, 1178*760c253cSXin Li step_fn, 1179*760c253cSXin Li result_from_json, 1180*760c253cSXin Li result_to_json, 1181*760c253cSXin Li ) 1182*760c253cSXin Li 1183*760c253cSXin Li if args.subparser_name == "create": 1184*760c253cSXin Li sudo_keepalive() 1185*760c253cSXin Li create_rust_uprev( 1186*760c253cSXin Li args.rust_version, args.template, args.skip_compile, run_step 1187*760c253cSXin Li ) 1188*760c253cSXin Li elif args.subparser_name == "remove": 1189*760c253cSXin Li remove_rust_uprev(args.rust_version, run_step) 1190*760c253cSXin Li else: 1191*760c253cSXin Li # If you have added more subparser_name, please also add the handlers 1192*760c253cSXin Li # above 1193*760c253cSXin Li assert args.subparser_name == "roll" 1194*760c253cSXin Li 1195*760c253cSXin Li sudo_keepalive() 1196*760c253cSXin Li # Determine the template version, if not given. 1197*760c253cSXin Li template_version = args.template 1198*760c253cSXin Li if template_version is None: 1199*760c253cSXin Li rust_ebuild = find_ebuild_for_package("dev-lang/rust") 1200*760c253cSXin Li template_version = RustVersion.parse_from_ebuild(rust_ebuild) 1201*760c253cSXin Li 1202*760c253cSXin Li run_step("create new repo", lambda: create_new_repo(args.uprev)) 1203*760c253cSXin Li if not args.skip_cross_compiler: 1204*760c253cSXin Li run_step( 1205*760c253cSXin Li "build cross compiler", 1206*760c253cSXin Li lambda: build_cross_compiler(template_version), 1207*760c253cSXin Li ) 1208*760c253cSXin Li create_rust_uprev( 1209*760c253cSXin Li args.uprev, template_version, args.skip_compile, run_step 1210*760c253cSXin Li ) 1211*760c253cSXin Li remove_rust_uprev(args.remove, run_step) 1212*760c253cSXin Li prepared = prepare_uprev_from_json(completed_steps["prepare uprev"]) 1213*760c253cSXin Li assert prepared is not None, "no prepared uprev decoded from JSON" 1214*760c253cSXin Li if not args.no_upload: 1215*760c253cSXin Li run_step( 1216*760c253cSXin Li "create rust uprev CL", lambda: create_new_commit(args.uprev) 1217*760c253cSXin Li ) 1218*760c253cSXin Li 1219*760c253cSXin Li 1220*760c253cSXin Liif __name__ == "__main__": 1221*760c253cSXin Li main() 1222