blob: 9106e1bf3eda39eb72bad9402ec20d0414023ddd [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.
"""Verify a given LLVM Branch Creation CL has the right patch stack.
The workflow generally for using this script is:
1. The CrOSTC Mage requests a new LLVM branch of the format
chromeos/llvm-rNNNN-N
2. The CrOSTC Mage updates the branch locally using
'ready_llvm_branch -r NNNNN --branch-number N --upload'
3. The reviewer runs this script on the TOP of the CL stack.
4. If this script exits with 0, the reviewer can run the provided
gerrit commands to approve and submit the CL stack.
This script exits with 0 if the branch has been verified, 1 if it failed
to verify.
Examples:
$ verify_patch_consistency.py --cl 5637483
$ verify_patch_consistency.py --chromiumos-root ~/chromiumos --cl 5637483
"""
import argparse
import json
from pathlib import Path
import re
import subprocess
import sys
import textwrap
from typing import Any, Dict, List, Tuple
from cros_utils import cros_paths
from cros_utils import git_utils
from llvm_tools import git_llvm_rev
from llvm_tools import llvm_project_base_commit
from llvm_tools import patch_utils
def verify_in_worktree(
toolchain_utils_dir: Path,
llvm_src_dir: Path,
patches_json: Path,
chromiumos_overlay: Path,
svn_revision: int,
cl_ref: str,
) -> bool:
"""Check equality between the local patches and an upstream CL reference.
Args:
toolchain_utils_dir: Path to toolchain_utils.
llvm_src_dir: Path to an llvm-project dir.
patches_json: A PATCHES.json file to apply.
chromiumos_overlay: Path to chromiumos-overlay
svn_revision: The synthetic SVN-style revision number to
determine which patches apply.
cl_ref: Upstream Change List reference name.
Returns:
True if the local patches match, False otherwise
"""
# We have to fetch to make sure we know that the matching_hash
# exists.
git_utils.fetch(
llvm_src_dir,
remote=git_utils.CROS_EXTERNAL_REMOTE,
branch=git_utils.CROS_MAIN_BRANCH,
)
matching_hash = git_llvm_rev.translate_rev_to_sha(
git_llvm_rev.LLVMConfig("origin", llvm_src_dir),
git_llvm_rev.Rev(git_llvm_rev.MAIN_BRANCH, svn_revision),
)
with git_utils.create_worktree(
llvm_src_dir, commitish=matching_hash
) as worktree_dir:
llvm_project_base_commit.make_base_commit(
toolchain_utils_dir,
worktree_dir,
chromiumos_overlay,
)
patch_utils.apply_all_from_json(
svn_version=svn_revision,
llvm_src_dir=worktree_dir,
patches_json_fp=patches_json,
patch_cmd=patch_utils.git_am_chromiumos_quiet,
)
# We have to fetch again inside the worktree for the CL itself.
git_utils.fetch(
worktree_dir,
remote=git_utils.CROS_EXTERNAL_REMOTE,
branch=cl_ref,
)
diff = ref_diff(worktree_dir, "HEAD", "FETCH_HEAD")
if diff:
local_head = git_utils.resolve_ref(worktree_dir, "HEAD")
fetch_head = git_utils.resolve_ref(worktree_dir, "FETCH_HEAD")
msg = (
f"FAILED TO VERIFY. Local patches and CL {cl_ref} differ!",
f"Comparing local HEAD {local_head} with"
f" FETCH_HEAD {fetch_head}",
"",
diff,
)
print("\n".join(msg), file=sys.stderr)
return False
return True
def ref_diff(cwd: Path, ref1: str, ref2: str) -> str:
"""Compute diff between two git refs."""
cmd = [
"git",
"diff",
ref1,
ref2,
"--",
]
return subprocess.run(
cmd,
cwd=cwd,
check=True,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8",
).stdout
def _gerrit_inspect(cl: int, chromiumos_root: Path) -> List[Dict[str, Any]]:
"""Gerrit command wrapper for easy mocking."""
cmd = ("gerrit", "--json", "inspect", str(cl))
return json.loads(
subprocess.run(
cmd,
cwd=chromiumos_root,
check=True,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8",
).stdout
)
def parse_branch(cl: int, chromiumos_root: Path) -> Tuple[int, str]:
"""Extract the LLVM synthetic revision and git ref from a CL branch."""
json_obj = _gerrit_inspect(cl, chromiumos_root)
branch_name = json_obj[0]["branch"]
ref = json_obj[0]["currentPatchSet"]["ref"]
branch_regex = re.compile(r"llvm-r(\d+)")
if match := branch_regex.search(branch_name):
return int(match.group(1)), ref
raise RuntimeError(
f"Could not parse SVN revision from CL {cl}'s branch: '{branch_name}'"
)
def _verified_message(svn_revision: int, cl: str, cl_ref: str) -> str:
"""Format the 'verified' message body and return it."""
gerrit_cmd_template = "gerrit %s $(gerrit --raw --no-pager deps '%s') 2"
gerrit_approve_cmd = gerrit_cmd_template % ("label-cr", cl)
gerrit_cq_cmd = gerrit_cmd_template % ("label-cq", cl)
return "-" * 80 + textwrap.dedent(
f"""
VERIFIED! Local patches for r{svn_revision} are identical to the
tree state at remote {cl_ref}. You can approve
these changes together with the 'gerrit' command:
{gerrit_approve_cmd}
Once approved, you can submit these changes with CQ+2:
{gerrit_cq_cmd}
"""
)
def parse_args(argv: List[str]) -> argparse.Namespace:
"""Parse passed in argv list."""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
chromiumos_root_action = parser.add_argument(
"--chromiumos-root",
type=Path,
help="""
Path to ChromiumOS root to detect the PATCHES.json.
If neither this flag nor --patch-file are specified, it is
autodetected.
""",
)
parser.add_argument(
"--patch-file",
type=Path,
help="""
Path to PATCHES.json. If not specified, it is autodetected
from --chromiumos-root.
""",
)
llvm_dir_action = parser.add_argument(
"--llvm-dir",
type=Path,
help="""
Path to a ChromiumOS llvm-project directory. If not
specified, it is autodetected from --chromiumos-root.
""",
)
parser.add_argument(
"--cl",
required=True,
type=int,
help="""
Top of patch stack CL for a given revision branch.
Expected to be in the format of just the CL number.
""",
)
args = parser.parse_args(argv)
# Set default chromiumos_root
if not args.chromiumos_root:
if repo_root := cros_paths.script_chromiumos_checkout():
args.chromiumos_root = repo_root
else:
raise argparse.ArgumentError(
chromiumos_root_action,
"Could not find chromiumos root automatically."
" Pass --chromiumos-root manually.",
)
# Set default llvm_dir
if not args.llvm_dir:
llvm_dir = args.chromiumos_root / cros_paths.LLVM_PROJECT
if not (llvm_dir / ".git").is_dir():
raise argparse.ArgumentError(
llvm_dir_action,
"Could not find llvm dir automatically. Pass --llvm-dir"
" manually.",
)
args.llvm_dir = llvm_dir
# Set default patch_file
if not args.patch_file:
args.patch_file = args.chromiumos_root / cros_paths.DEFAULT_PATCHES_PATH
return args
def main(argv: List[str]) -> int:
"""Entry point."""
args = parse_args(argv)
svn_revision, cl_ref = parse_branch(args.cl, args.chromiumos_root)
if not verify_in_worktree(
toolchain_utils_dir=args.chromiumos_root / cros_paths.TOOLCHAIN_UTILS,
llvm_src_dir=args.llvm_dir,
patches_json=args.patch_file,
chromiumos_overlay=args.chromiumos_root / cros_paths.CHROMIUMOS_OVERLAY,
svn_revision=svn_revision,
cl_ref=cl_ref,
):
return 1
print(_verified_message(svn_revision, args.cl, cl_ref))
return 0