xref: /aosp_15_r20/external/toolchain-utils/rust_tools/rust_uprev.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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