• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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