blob: e207247841d7527c2785d2793bb2dd72d7b7e7b3 [file] [log] [blame]
# Copyright 2024 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Uploads CLs necessary to run LLVM testing at an arbitrary SHA.
Also has the ability to:
- kick off a CQ run
- keep track of the last SHA that testing was requested on, and skip
re-uploading if the SHA has not changed.
"""
import argparse
import dataclasses
import json
import logging
from pathlib import Path
import subprocess
import textwrap
from typing import List, Optional
from cros_utils import git_utils
from llvm_tools import atomic_write_file
from llvm_tools import chroot
from llvm_tools import get_llvm_hash
from llvm_tools import llvm_next
from llvm_tools import manifest_utils
from llvm_tools import upload_llvm_testing_helper_cl
def resolve_llvm_sha(sha_or_special: str) -> str:
"""Resolves the `--sha` flag to an LLVM SHA."""
if sha_or_special == "llvm-next":
return llvm_next.LLVM_NEXT_HASH
if sha_or_special == "google3":
return get_llvm_hash.LLVMHash().GetGoogle3LLVMHash()
if sha_or_special == "google3-unstable":
return get_llvm_hash.LLVMHash().GetGoogle3LLVMHash()
# If this looks like a full git SHA, there's no need to sync the upstream
# repo.
if git_utils.is_full_git_sha(sha_or_special):
return sha_or_special
return git_utils.resolve_ref(
get_llvm_hash.GetCachedUpToDateReadOnlyLLVMRepo().path, sha_or_special
)
def read_last_tried_sha(retry_state: Path) -> Optional[str]:
"""Reads the last tried SHA from the state file."""
try:
with retry_state.open(encoding="utf-8") as f:
return json.load(f)["last_tried_sha"]
except FileNotFoundError:
return None
def write_last_tried_sha(retry_state: Path, sha: str):
"""Writes the last tried SHA to the state file."""
with atomic_write_file.atomic_write(retry_state) as f:
json.dump({"last_tried_sha": sha}, f)
@dataclasses.dataclass(frozen=True)
class UploadedCLs:
"""Listing of CL numbers uploaded by a function."""
internal: List[int]
external: List[int]
def upload_one_cl_to_main(
git_dir: Path, sha: str, remote: str, topic: Optional[str] = None
) -> int:
"""Uploads exactly one SHA from `git_dir`. Returns the CL number.
Raises:
AssertionError if more than one CL was uploaded.
"""
cl_ids = git_utils.upload_to_gerrit(
git_dir,
remote=remote,
branch=git_utils.CROS_MAIN_BRANCH,
ref=sha,
topic=topic,
)
assert len(cl_ids) == 1, f"Expected to upload one CL; uploaded {cl_ids}"
return cl_ids[0]
def create_and_upload_test_helpers_cl(
chromeos_tree: Path,
dry_run: bool,
tot: bool,
) -> int:
"""Creates & uploads the LLVM 'test helper' CL.
Returns:
The CL number of the test-helper CL, an int referencing an external CL.
If dry_run is passed, returns 0.
"""
chromiumos_overlay = (
chromeos_tree / "src" / "third_party" / "chromiumos-overlay"
)
sha = upload_llvm_testing_helper_cl.create_helper_cl_commit_in_worktree_of(
chromiumos_overlay, tot
)
if dry_run:
logging.info(
"--dry-run passed; skipping upload of test-helpers CL %s", sha
)
return 0
return upload_one_cl_to_main(
chromiumos_overlay, sha, remote=git_utils.CROS_EXTERNAL_REMOTE
)
def build_manifest_commit_message(
llvm_sha: str,
llvm_rev: int,
cq_depend_external: Optional[int],
) -> str:
msg = textwrap.dedent(
f"""\
toolchain.xml: update llvm to {llvm_sha} (r{llvm_rev})
BUG=None
TEST=CQ
"""
)
if cq_depend_external:
msg += f"\n\nCq-Depend: chromium:{cq_depend_external}"
return msg
def create_and_upload_manifest_cl(
*,
chromeos_tree: Path,
llvm_sha: str,
llvm_rev: int,
cq_depend_external: Optional[int],
dry_run: bool,
topic: Optional[str],
tot: bool,
) -> int:
"""Creates & uploads the LLVM update manifest CL.
Returns:
The CL number of the manifest CL, an int referencing an internal CL. If
dry_run is passed, returns `0`.
"""
manifest_internal = chromeos_tree / "manifest-internal"
remote = git_utils.CROS_INTERNAL_REMOTE
with git_utils.create_worktree(manifest_internal) as worktree:
if tot:
git_utils.fetch_and_checkout(
worktree,
remote=remote,
branch=git_utils.CROS_MAIN_BRANCH,
)
manifest_utils.update_chromeos_manifest_in_manifest_dir(
llvm_sha,
worktree,
chromeos_tree=chromeos_tree,
)
commit_msg = build_manifest_commit_message(
llvm_sha, llvm_rev, cq_depend_external
)
sha = git_utils.commit_all_changes(worktree, commit_msg)
if dry_run:
logging.info("--dry-run passed; skipping upload of manifest CL %s", sha)
return 0
return upload_one_cl_to_main(
manifest_internal,
sha,
remote=remote,
topic=topic,
)
def add_cl_comment(
chromeos_tree: Path,
cl_id: int,
internal: bool,
comment: str,
):
"""Creates & uploads the LLVM update manifest CL.
Returns:
The CL number of the manifest CL, an int referencing an internal CL.
"""
cmd = ["gerrit"]
if internal:
cmd.append("--internal")
cmd += ("message", str(cl_id), comment)
subprocess.run(
cmd,
check=True,
cwd=chromeos_tree,
stdin=subprocess.DEVNULL,
)
def create_and_upload_cls(
*,
chromeos_tree: Path,
llvm_sha: str,
llvm_rev: int,
include_test_helpers: bool,
dry_run: bool,
manifest_gerrit_topic: Optional[str],
tot: bool,
) -> UploadedCLs:
external_cls = []
if include_test_helpers:
logging.info("Uploading test-helper CL...")
test_helper_cl = create_and_upload_test_helpers_cl(
chromeos_tree, dry_run, tot
)
external_cls.append(test_helper_cl)
else:
test_helper_cl = None
logging.info("Creating LLVM update CL...")
manifest_cl = create_and_upload_manifest_cl(
chromeos_tree=chromeos_tree,
llvm_sha=llvm_sha,
llvm_rev=llvm_rev,
cq_depend_external=test_helper_cl,
dry_run=dry_run,
topic=manifest_gerrit_topic,
tot=tot,
)
# Notably, this is meant to catch `test_helper_cl == 0` (dry_run) or
# `test_helper_cl == None` (if none was uploaded)
if test_helper_cl:
add_cl_comment(
chromeos_tree,
test_helper_cl,
internal=False,
comment=f"Corresponding Manifest update: crrev.com/i/{manifest_cl}",
)
return UploadedCLs(
internal=[manifest_cl],
external=external_cls,
)
def make_gerrit_cq_dry_run_command(cls: List[int], internal: bool) -> List[str]:
assert cls, "Can't make a dry-run command with no CLs to dry-run."
cmd = ["gerrit"]
if internal:
cmd.append("--internal")
cmd.append("label-cq")
cmd += (str(x) for x in cls)
cmd.append("1")
return cmd
def cq_dry_run_cls(chromeos_tree: Path, cls: UploadedCLs):
"""Sets CQ+1 on the given uploaded CL listing."""
# At the time of writing, this is expected given the context of the script.
# Can easily refactor to make `cls.internal` optional, though.
gerrit_cmds = []
assert cls.internal, "LLVM update without internal CLs?"
gerrit_cmds.append(
make_gerrit_cq_dry_run_command(cls.internal, internal=True)
)
if cls.external:
gerrit_cmds.append(
make_gerrit_cq_dry_run_command(cls.external, internal=False)
)
for cmd in gerrit_cmds:
subprocess.run(
cmd,
check=True,
cwd=chromeos_tree,
stdin=subprocess.DEVNULL,
)
def parse_opts(argv: List[str]) -> argparse.Namespace:
"""Parse command-line options."""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--chromeos-tree",
type=Path,
help="""
ChromeOS tree to make modifications in. Will be inferred if none
is passed.
""",
)
parser.add_argument(
"--cq",
action="store_true",
help="After uploading, set CQ+1 on the CL(s) that were uploaded.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="If passed, only commit changes locally; don't upload them.",
)
parser.add_argument(
"--include-llvm-test-helper-cls",
action="store_true",
help="""
Also upload CL(s) meant to ease LLVM testing. Namely, this will include
logic to disable `-Werror` on packages, and logic to disable patches
that no longer apply to LLVM.
""",
)
parser.add_argument(
"--manifest-gerrit-topic",
help="""
If provided, the internal-manifest CL will be uploaded with the given
Gerrit topic. This is helpful to associate many CLs over time.
""",
)
parser.add_argument(
"--tot",
action="store_true",
help="""
If passed, modified repos will be `git fetch`ed and this script will
work on their main branches, rather than working on the version you
have locally.
""",
)
parser.add_argument(
"--retry-state",
type=Path,
help="""
If passed, this will keep script state in the given file. At the
moment, this file is only used to ensure that subsequent runs of this
script don't trigger identical uploads.
""",
)
parser.add_argument(
"--sha",
required=True,
help="""
SHA to use. This can either be an LLVM SHA, or a special value:
`llvm-next`, `google3` or `google3-unstable`.
""",
)
return parser.parse_args(argv)
def main(argv: List[str]) -> None:
logging.basicConfig(
format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
"%(message)s",
level=logging.INFO,
)
opts = parse_opts(argv)
dry_run = opts.dry_run
chromeos_tree = opts.chromeos_tree
if not chromeos_tree:
chromeos_tree = chroot.FindChromeOSRootAboveToolchainUtils()
new_sha = resolve_llvm_sha(opts.sha)
logging.info("Using LLVM SHA %s...", new_sha)
if opts.retry_state:
last_tried_sha = read_last_tried_sha(opts.retry_state)
if last_tried_sha == new_sha:
logging.info("New SHA is the same as the last tried SHA; quit.")
return
logging.info(
"New SHA is different than the last tried SHA (%s).", last_tried_sha
)
logging.info("Getting LLVM revision for SHA %s...", new_sha)
new_rev = (
get_llvm_hash.GetCachedUpToDateReadOnlyLLVMRepo().GetRevisionFromHash(
new_sha
)
)
logging.info("LLVM SHA %s == r%d", new_sha, new_rev)
uploaded_cls = create_and_upload_cls(
chromeos_tree=chromeos_tree,
llvm_sha=new_sha,
llvm_rev=new_rev,
include_test_helpers=opts.include_llvm_test_helper_cls,
dry_run=dry_run,
manifest_gerrit_topic=opts.manifest_gerrit_topic,
tot=opts.tot,
)
if dry_run:
logging.info("--dry-run passed; exiting")
return
if opts.cq:
logging.info("Setting CQ+1 on the CLs...")
cq_dry_run_cls(chromeos_tree, uploaded_cls)
if opts.retry_state:
write_last_tried_sha(opts.retry_state, new_sha)