aboutsummaryrefslogtreecommitdiff
path: root/rust_tools/rust_uprev.py
diff options
context:
space:
mode:
Diffstat (limited to 'rust_tools/rust_uprev.py')
-rwxr-xr-xrust_tools/rust_uprev.py371
1 files changed, 299 insertions, 72 deletions
diff --git a/rust_tools/rust_uprev.py b/rust_tools/rust_uprev.py
index 382d991a..e9113ea7 100755
--- a/rust_tools/rust_uprev.py
+++ b/rust_tools/rust_uprev.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
# Copyright 2020 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
@@ -36,34 +35,98 @@ import re
import shlex
import shutil
import subprocess
-import sys
-from typing import Any, Callable, Dict, List, NamedTuple, Optional, T, Tuple
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ List,
+ NamedTuple,
+ Optional,
+ Protocol,
+ Tuple,
+ TypeVar,
+ Union,
+)
+import urllib.request
from llvm_tools import chroot
from llvm_tools import git
+from pgo_tools_rust import pgo_rust
+
+
+T = TypeVar("T")
+Command = List[Union[str, os.PathLike]]
+PathOrStr = Union[str, os.PathLike]
+
+
+class RunStepFn(Protocol):
+ """Protocol that corresponds to run_step's type.
+
+ This can be used as the type of a function parameter that accepts
+ run_step as its value.
+ """
+
+ def __call__(
+ self,
+ step_name: str,
+ step_fn: Callable[[], T],
+ result_from_json: Optional[Callable[[Any], T]] = None,
+ result_to_json: Optional[Callable[[T], Any]] = None,
+ ) -> T:
+ ...
EQUERY = "equery"
-GSUTIL = "gsutil.py"
+GPG = "gpg"
+GSUTIL = "gsutil"
MIRROR_PATH = "gs://chromeos-localmirror/distfiles"
EBUILD_PREFIX = Path("/mnt/host/source/src/third_party/chromiumos-overlay")
+CROS_RUSTC_ECLASS = EBUILD_PREFIX / "eclass/cros-rustc.eclass"
+# Keyserver to use with GPG. Not all keyservers have Rust's signing key;
+# this must be set to a keyserver that does.
+GPG_KEYSERVER = "keyserver.ubuntu.com"
RUST_PATH = Path(EBUILD_PREFIX, "dev-lang", "rust")
+# This is the signing key used by upstream Rust as of 2023-08-09.
+# If the project switches to a different key, this will have to be updated.
+# We require the key to be updated manually so that we have an opportunity
+# to verify that the key change is legitimate.
+RUST_SIGNING_KEY = "85AB96E6FA1BE5FE"
+RUST_SRC_BASE_URI = "https://static.rust-lang.org/dist/"
+
+class SignatureVerificationError(Exception):
+ """Error that indicates verification of a downloaded file failed.
-def get_command_output(command: List[str], *args, **kwargs) -> str:
+ Attributes:
+ message: explanation of why the verification failed.
+ path: the path to the file whose integrity was being verified.
+ """
+
+ def __init__(self, message: str, path: Path):
+ super(SignatureVerificationError, self).__init__()
+ self.message = message
+ self.path = path
+
+
+def get_command_output(command: Command, *args, **kwargs) -> str:
return subprocess.check_output(
command, encoding="utf-8", *args, **kwargs
).strip()
-def get_command_output_unchecked(command: List[str], *args, **kwargs) -> str:
+def get_command_output_unchecked(command: Command, *args, **kwargs) -> str:
+ # pylint: disable=subprocess-run-check
return subprocess.run(
command,
- check=False,
- stdout=subprocess.PIPE,
- encoding="utf-8",
*args,
- **kwargs,
+ **dict(
+ {
+ "check": False,
+ "stdout": subprocess.PIPE,
+ "encoding": "utf-8",
+ },
+ **kwargs,
+ ),
).stdout.strip()
@@ -109,6 +172,14 @@ class RustVersion(NamedTuple):
)
+class PreparedUprev(NamedTuple):
+ """Container for the information returned by prepare_uprev."""
+
+ template_version: RustVersion
+ ebuild_path: Path
+ bootstrap_version: RustVersion
+
+
def compute_rustc_src_name(version: RustVersion) -> str:
return f"rustc-{version}-src.tar.gz"
@@ -117,7 +188,7 @@ def compute_rust_bootstrap_prebuilt_name(version: RustVersion) -> str:
return f"rust-bootstrap-{version}.tbz2"
-def find_ebuild_for_package(name: str) -> os.PathLike:
+def find_ebuild_for_package(name: str) -> str:
"""Returns the path to the ebuild for the named package."""
return get_command_output([EQUERY, "w", name])
@@ -303,7 +374,7 @@ def parse_commandline_args() -> argparse.Namespace:
def prepare_uprev(
rust_version: RustVersion, template: Optional[RustVersion]
-) -> Optional[Tuple[RustVersion, str, RustVersion]]:
+) -> Optional[PreparedUprev]:
if template is None:
ebuild_path = find_ebuild_for_package("rust")
ebuild_name = os.path.basename(ebuild_path)
@@ -329,32 +400,11 @@ def prepare_uprev(
)
logging.info("rust-bootstrap version is %s", bootstrap_version)
- return template_version, ebuild_path, bootstrap_version
-
-
-def copy_patches(
- directory: Path, template_version: RustVersion, new_version: RustVersion
-) -> None:
- patch_path = directory / "files"
- prefix = "%s-%s-" % (directory.name, template_version)
- new_prefix = "%s-%s-" % (directory.name, new_version)
- for f in os.listdir(patch_path):
- if not f.startswith(prefix):
- continue
- logging.info("Copy patch %s to new version", f)
- new_name = f.replace(str(template_version), str(new_version))
- shutil.copyfile(
- os.path.join(patch_path, f),
- os.path.join(patch_path, new_name),
- )
-
- subprocess.check_call(
- ["git", "add", f"{new_prefix}*.patch"], cwd=patch_path
- )
+ return PreparedUprev(template_version, Path(ebuild_path), bootstrap_version)
def create_ebuild(
- template_ebuild: str, pkgatom: str, new_version: RustVersion
+ template_ebuild: PathOrStr, pkgatom: str, new_version: RustVersion
) -> str:
filename = f"{Path(pkgatom).name}-{new_version}.ebuild"
ebuild = EBUILD_PREFIX.joinpath(f"{pkgatom}/{filename}")
@@ -363,11 +413,38 @@ def create_ebuild(
return str(ebuild)
+def set_include_profdata_src(ebuild_path: os.PathLike, include: bool) -> None:
+ """Changes an ebuild file to include or omit profile data from SRC_URI.
+
+ If include is True, the ebuild file will be rewritten to include
+ profile data in SRC_URI.
+
+ If include is False, the ebuild file will be rewritten to omit profile
+ data from SRC_URI.
+ """
+ if include:
+ old = ""
+ new = "yes"
+ else:
+ old = "yes"
+ new = ""
+ contents = Path(ebuild_path).read_text(encoding="utf-8")
+ contents, subs = re.subn(
+ f"^INCLUDE_PROFDATA_IN_SRC_URI={old}$",
+ f"INCLUDE_PROFDATA_IN_SRC_URI={new}",
+ contents,
+ flags=re.MULTILINE,
+ )
+ # We expect exactly one substitution.
+ assert subs == 1, "Failed to update INCLUDE_PROFDATA_IN_SRC_URI"
+ Path(ebuild_path).write_text(contents, encoding="utf-8")
+
+
def update_bootstrap_ebuild(new_bootstrap_version: RustVersion) -> None:
old_ebuild = find_ebuild_path(rust_bootstrap_path(), "rust-bootstrap")
- m = re.match(r"^rust-bootstrap-(\d+).(\d+).(\d+)", old_ebuild.name)
+ m = re.match(r"^rust-bootstrap-(\d+.\d+.\d+)", old_ebuild.name)
assert m, old_ebuild.name
- old_version = RustVersion(m.group(1), m.group(2), m.group(3))
+ old_version = RustVersion.parse(m.group(1))
new_ebuild = old_ebuild.parent.joinpath(
f"rust-bootstrap-{new_bootstrap_version}.ebuild"
)
@@ -383,9 +460,10 @@ def update_bootstrap_ebuild(new_bootstrap_version: RustVersion) -> None:
def update_bootstrap_version(
- path: str, new_bootstrap_version: RustVersion
+ path: PathOrStr, new_bootstrap_version: RustVersion
) -> None:
- contents = open(path, encoding="utf-8").read()
+ path = Path(path)
+ contents = path.read_text(encoding="utf-8")
contents, subs = re.subn(
r"^BOOTSTRAP_VERSION=.*$",
'BOOTSTRAP_VERSION="%s"' % (new_bootstrap_version,),
@@ -394,7 +472,7 @@ def update_bootstrap_version(
)
if not subs:
raise RuntimeError(f"BOOTSTRAP_VERSION not found in {path}")
- open(path, "w", encoding="utf-8").write(contents)
+ path.write_text(contents, encoding="utf-8")
logging.info("Rust BOOTSTRAP_VERSION updated to %s", new_bootstrap_version)
@@ -426,7 +504,7 @@ def fetch_distfile_from_mirror(name: str) -> None:
"""
mirror_file = MIRROR_PATH + "/" + name
local_file = Path(get_distdir(), name)
- cmd = [GSUTIL, "cp", mirror_file, local_file]
+ cmd: Command = [GSUTIL, "cp", mirror_file, local_file]
logging.info("Running %r", cmd)
rc = subprocess.call(cmd)
if rc != 0:
@@ -499,12 +577,116 @@ def fetch_rust_distfiles(version: RustVersion) -> None:
fetch_distfile_from_mirror(compute_rustc_src_name(version))
-def get_distdir() -> os.PathLike:
+def fetch_rust_src_from_upstream(uri: str, local_path: Path) -> None:
+ """Fetches Rust sources from upstream.
+
+ This downloads the source distribution and the .asc file
+ containing the signatures. It then verifies that the sources
+ have the expected signature and have been signed by
+ the expected key.
+ """
+ subprocess.run(
+ [GPG, "--keyserver", GPG_KEYSERVER, "--recv-keys", RUST_SIGNING_KEY],
+ check=True,
+ )
+ subprocess.run(
+ [GPG, "--keyserver", GPG_KEYSERVER, "--refresh-keys", RUST_SIGNING_KEY],
+ check=True,
+ )
+ asc_uri = uri + ".asc"
+ local_asc_path = Path(local_path.parent, local_path.name + ".asc")
+ logging.info("Fetching %s", uri)
+ urllib.request.urlretrieve(uri, local_path)
+ logging.info("%s fetched", uri)
+
+ # Raise SignatureVerificationError if we cannot get the signature.
+ try:
+ logging.info("Fetching %s", asc_uri)
+ urllib.request.urlretrieve(asc_uri, local_asc_path)
+ logging.info("%s fetched", asc_uri)
+ except Exception as e:
+ raise SignatureVerificationError(
+ f"error fetching signature file {asc_uri}",
+ local_path,
+ ) from e
+
+ # Raise SignatureVerificationError if verifying the signature
+ # failed.
+ try:
+ output = get_command_output(
+ [GPG, "--verify", "--status-fd", "1", local_asc_path]
+ )
+ except subprocess.CalledProcessError as e:
+ raise SignatureVerificationError(
+ f"error verifying signature. GPG output:\n{e.stdout}",
+ local_path,
+ ) from e
+
+ # Raise SignatureVerificationError if the file was not signed
+ # with the expected key.
+ if f"GOODSIG {RUST_SIGNING_KEY}" not in output:
+ message = f"GOODSIG {RUST_SIGNING_KEY} not found in output"
+ if f"REVKEYSIG {RUST_SIGNING_KEY}" in output:
+ message = "signing key has been revoked"
+ elif f"EXPKEYSIG {RUST_SIGNING_KEY}" in output:
+ message = "signing key has expired"
+ elif f"EXPSIG {RUST_SIGNING_KEY}" in output:
+ message = "signature has expired"
+ raise SignatureVerificationError(
+ f"{message}. GPG output:\n{output}",
+ local_path,
+ )
+
+
+def get_distdir() -> str:
"""Returns portage's distdir."""
return get_command_output(["portageq", "distdir"])
-def update_manifest(ebuild_file: os.PathLike) -> None:
+def mirror_has_file(name: str) -> bool:
+ """Checks if the mirror has the named file."""
+ mirror_file = MIRROR_PATH + "/" + name
+ cmd: Command = [GSUTIL, "ls", mirror_file]
+ proc = subprocess.run(
+ cmd,
+ check=False,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ encoding="utf-8",
+ )
+ if "URLs matched no objects" in proc.stdout:
+ return False
+ elif proc.returncode == 0:
+ return True
+
+ raise Exception(
+ "Unexpected result from gsutil ls:"
+ f" rc {proc.returncode} output:\n{proc.stdout}"
+ )
+
+
+def mirror_rust_source(version: RustVersion) -> None:
+ """Ensures source code for a Rust version is on the local mirror.
+
+ If the source code is not found on the mirror, it is fetched
+ from upstream, its integrity is verified, and it is uploaded
+ to the mirror.
+ """
+ filename = compute_rustc_src_name(version)
+ if mirror_has_file(filename):
+ logging.info("%s is present on the mirror", filename)
+ return
+ uri = f"{RUST_SRC_BASE_URI}{filename}"
+ local_path = Path(get_distdir()) / filename
+ mirror_path = f"{MIRROR_PATH}/{filename}"
+ fetch_rust_src_from_upstream(uri, local_path)
+ subprocess.run(
+ [GSUTIL, "cp", "-a", "public-read", local_path, mirror_path],
+ check=True,
+ )
+
+
+def update_manifest(ebuild_file: PathOrStr) -> None:
"""Updates the MANIFEST for the ebuild at the given path."""
ebuild = Path(ebuild_file)
ebuild_actions(ebuild.parent.name, ["manifest"])
@@ -596,28 +778,57 @@ def perform_step(
return val
-def prepare_uprev_from_json(
- obj: Any,
-) -> Optional[Tuple[RustVersion, str, RustVersion]]:
+def prepare_uprev_from_json(obj: Any) -> Optional[PreparedUprev]:
if not obj:
return None
version, ebuild_path, bootstrap_version = obj
- return RustVersion(*version), ebuild_path, RustVersion(*bootstrap_version)
+ return PreparedUprev(
+ RustVersion(*version),
+ Path(ebuild_path),
+ RustVersion(*bootstrap_version),
+ )
+
+
+def prepare_uprev_to_json(
+ prepared_uprev: Optional[PreparedUprev],
+) -> Optional[Tuple[RustVersion, str, RustVersion]]:
+ if prepared_uprev is None:
+ return None
+ return (
+ prepared_uprev.template_version,
+ str(prepared_uprev.ebuild_path),
+ prepared_uprev.bootstrap_version,
+ )
def create_rust_uprev(
rust_version: RustVersion,
maybe_template_version: Optional[RustVersion],
skip_compile: bool,
- run_step: Callable[[], T],
+ run_step: RunStepFn,
) -> None:
- template_version, template_ebuild, old_bootstrap_version = run_step(
+ prepared = run_step(
"prepare uprev",
lambda: prepare_uprev(rust_version, maybe_template_version),
result_from_json=prepare_uprev_from_json,
+ result_to_json=prepare_uprev_to_json,
)
- if template_ebuild is None:
+ if prepared is None:
return
+ template_version, template_ebuild, old_bootstrap_version = prepared
+
+ run_step(
+ "mirror bootstrap sources",
+ lambda: mirror_rust_source(
+ template_version,
+ ),
+ )
+ run_step(
+ "mirror rust sources",
+ lambda: mirror_rust_source(
+ rust_version,
+ ),
+ )
# The fetch steps will fail (on purpose) if the files they check for
# are not available on the mirror. To make them pass, fetch the
@@ -644,13 +855,11 @@ def create_rust_uprev(
)
run_step(
"update bootstrap version",
- lambda: update_bootstrap_version(
- EBUILD_PREFIX.joinpath("eclass/cros-rustc.eclass"), template_version
- ),
+ lambda: update_bootstrap_version(CROS_RUSTC_ECLASS, template_version),
)
run_step(
- "copy patches",
- lambda: copy_patches(RUST_PATH, template_version, rust_version),
+ "turn off profile data sources in cros-rustc.eclass",
+ lambda: set_include_profdata_src(CROS_RUSTC_ECLASS, include=False),
)
template_host_ebuild = EBUILD_PREFIX.joinpath(
f"dev-lang/rust-host/rust-host-{template_version}.ebuild"
@@ -673,6 +882,26 @@ def create_rust_uprev(
"update target manifest to add new version",
lambda: update_manifest(Path(target_file)),
)
+ run_step(
+ "generate profile data for rustc",
+ lambda: pgo_rust.main(["pgo_rust", "generate"]),
+ )
+ run_step(
+ "upload profile data for rustc",
+ lambda: pgo_rust.main(["pgo_rust", "upload-profdata"]),
+ )
+ run_step(
+ "turn on profile data sources in cros-rustc.eclass",
+ lambda: set_include_profdata_src(CROS_RUSTC_ECLASS, include=True),
+ )
+ run_step(
+ "update host manifest to add profile data",
+ lambda: update_manifest(Path(host_file)),
+ )
+ run_step(
+ "update target manifest to add profile data",
+ lambda: update_manifest(Path(target_file)),
+ )
if not skip_compile:
run_step("build packages", lambda: rebuild_packages(rust_version))
run_step(
@@ -755,16 +984,16 @@ def rebuild_packages(version: RustVersion):
raise
-def remove_ebuild_version(path: os.PathLike, name: str, version: RustVersion):
+def remove_ebuild_version(path: PathOrStr, name: str, version: RustVersion):
"""Remove the specified version of an ebuild.
Removes {path}/{name}-{version}.ebuild and {path}/{name}-{version}-*.ebuild
using git rm.
Args:
- path: The directory in which the ebuild files are.
- name: The name of the package (e.g. 'rust').
- version: The version of the ebuild to remove.
+ path: The directory in which the ebuild files are.
+ name: The name of the package (e.g. 'rust').
+ version: The version of the ebuild to remove.
"""
path = Path(path)
pattern = f"{name}-{version}-*.ebuild"
@@ -780,12 +1009,13 @@ def remove_ebuild_version(path: os.PathLike, name: str, version: RustVersion):
remove_files(m.name, path)
-def remove_files(filename: str, path: str) -> None:
+def remove_files(filename: PathOrStr, path: PathOrStr) -> None:
subprocess.check_call(["git", "rm", filename], cwd=path)
def remove_rust_bootstrap_version(
- version: RustVersion, run_step: Callable[[], T]
+ version: RustVersion,
+ run_step: RunStepFn,
) -> None:
run_step(
"remove old bootstrap ebuild",
@@ -801,7 +1031,8 @@ def remove_rust_bootstrap_version(
def remove_rust_uprev(
- rust_version: Optional[RustVersion], run_step: Callable[[], T]
+ rust_version: Optional[RustVersion],
+ run_step: RunStepFn,
) -> None:
def find_desired_rust_version() -> RustVersion:
if rust_version:
@@ -817,10 +1048,6 @@ def remove_rust_uprev(
result_from_json=find_desired_rust_version_from_json,
)
run_step(
- "remove patches",
- lambda: remove_files(f"files/rust-{delete_version}-*.patch", RUST_PATH),
- )
- run_step(
"remove target ebuild",
lambda: remove_ebuild_version(RUST_PATH, "rust", delete_version),
)
@@ -963,7 +1190,8 @@ def main() -> None:
elif args.subparser_name == "remove-bootstrap":
remove_rust_bootstrap_version(args.version, run_step)
else:
- # If you have added more subparser_name, please also add the handlers above
+ # If you have added more subparser_name, please also add the handlers
+ # above
assert args.subparser_name == "roll"
run_step("create new repo", lambda: create_new_repo(args.uprev))
if not args.skip_cross_compiler:
@@ -972,10 +1200,9 @@ def main() -> None:
args.uprev, args.template, args.skip_compile, run_step
)
remove_rust_uprev(args.remove, run_step)
- bootstrap_version = prepare_uprev_from_json(
- completed_steps["prepare uprev"]
- )[2]
- remove_rust_bootstrap_version(bootstrap_version, run_step)
+ prepared = prepare_uprev_from_json(completed_steps["prepare uprev"])
+ assert prepared is not None, "no prepared uprev decoded from JSON"
+ remove_rust_bootstrap_version(prepared.bootstrap_version, run_step)
if not args.no_upload:
run_step(
"create rust uprev CL", lambda: create_new_commit(args.uprev)
@@ -983,4 +1210,4 @@ def main() -> None:
if __name__ == "__main__":
- sys.exit(main())
+ main()