blob: 25edaf5fb4e1973a8a31134ae0f724af180fc8c6 [file] [edit]
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Runs necessary checkpatch targets for a build on ci.android.com.
usage:
tools/bazel run //build/kernel/static_analysis:checkpatch_presubmit -- \\
--dist_dir <DIST_DIR> \\
[<other flags to checkpatch>] \\
"""
import argparse
import collections
import json
import logging
import os
import pathlib
import shlex
import subprocess
import sys
from typing import Any
_LOG_LEVEL = logging.INFO
# _LOG_LEVEL = logging.DEBUG
_SILENT_ARGS = [
"--ui_event_filters=-info",
"--noshow_progress",
]
# TODO: Find a better way to handle this exceptions;
_PATH_PREFIX_DENY_LIST = (
"external/",
"bootable/",
"prebuilts/fuchsia_sdk",
"test/dittosuite",
)
def load_arguments() -> dict[str, Any]:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(
"--dist_dir",
type=_resolve_against_workspace_root,
required=True,
help="DIST_DIR. If relative, resolve against workspace root.",
)
parser.add_argument(
"--bid",
help="Build ID. If specified, it is used to skip the check on post-submit.",
)
parser.add_argument(
"--change_info",
type=_require_absolute_path,
required=True,
help="Path to change-info file providing complete change information.",
)
parser.add_argument("--bazel_wrapper",
type=pathlib.Path,
help="Path to Kleaf's Bazel wrapper")
return parser.parse_known_args()
def _resolve_against_workspace_root(value: str) -> pathlib.Path:
path = pathlib.Path(value)
if path.is_absolute():
return path
return pathlib.Path(os.environ["BUILD_WORKSPACE_DIRECTORY"]) / path
def _require_absolute_path(p: str | pathlib.Path) -> pathlib.Path:
p = pathlib.Path(p)
if not p.is_absolute():
raise argparse.ArgumentTypeError("need to specify an absolute path")
return p
def _log_command(args):
quoted = [shlex.quote(str(arg)) for arg in args]
logging.debug("Running command line: %s", " ".join(quoted))
def _find_checkpatch_targets(bazel_wrapper: pathlib.Path, path: pathlib.Path) \
-> list[str]:
if str(path).startswith(_PATH_PREFIX_DENY_LIST):
logging.info("Skipped //%s path in deny list", path)
return []
if not _resolve_against_workspace_root(path / "BUILD.bazel").is_file() and \
not _resolve_against_workspace_root(path / "BUILD").is_file():
logging.info("//%s is not a package; no BUILD file is found", path)
return []
args = [str(bazel_wrapper.absolute()), "query"]
args += _SILENT_ARGS
args.append(f'kind("^checkpatch rule$", //{path}:all)')
_log_command(args)
lines = subprocess.check_output(
args,
text=True,
cwd=_resolve_against_workspace_root("."),
env=_env_for_recursive_bazel_calls(),
).splitlines()
return [line.strip() for line in lines if line.strip()]
def _run_checkpatch(
bazel_wrapper: pathlib.Path,
target: str,
git_sha1: str,
log: pathlib.Path,
checkpatch_args: list[str],
silent: bool = False,
) -> int:
args = [str(bazel_wrapper.absolute()), "run", "--show_result=0"]
args += _SILENT_ARGS
args += [target, "--"]
args += checkpatch_args
args += ["--log", log]
args += ["--git_sha1", git_sha1]
_log_command(args)
return subprocess.run(
args,
text=True,
cwd=_resolve_against_workspace_root("."),
stdout=subprocess.DEVNULL if silent else None,
stderr=subprocess.STDOUT if silent else None,
env=_env_for_recursive_bazel_calls(),
).returncode
def _env_for_recursive_bazel_calls() -> dict[str, str]:
env = os.environ.copy()
# Set PYTHONSAFEPATH to empty string:
# - so that rules_python stage1 script won't override it with 1, and
# import-ing a module from py_binary.deps works.
env["PYTHONSAFEPATH"] = ""
# Use an alternate lock file so that the lock held by the
# parent process won't effect the test.
env["KLEAF_BAZELRC_SUBDIR"] = "checkpatch"
return env
def _find_repo(curdir: pathlib.Path) -> pathlib.Path | None:
"""Find repo installation."""
while curdir.parent != curdir: # is not root
maybe_dot_repo = curdir / ".repo"
if maybe_dot_repo.is_dir():
return curdir
curdir = curdir.parent
return None
def _get_package_path(path: pathlib.Path) -> pathlib.Path | None:
"""Get package path from project path"""
workspace_dir = pathlib.Path(os.environ["BUILD_WORKSPACE_DIRECTORY"])
repo_root_s = os.environ.get("KLEAF_REPO_MANIFEST", ":").split(":")[0]
if repo_root_s:
repo_root = pathlib.Path(repo_root_s).resolve()
else:
repo_root = _find_repo(workspace_dir)
if not repo_root:
logging.error(
"Unable to determine repo root. Please specify --repo_manifest.")
return None
realpath = repo_root / path
if realpath.is_relative_to(workspace_dir):
return realpath.relative_to(workspace_dir)
return None
def main(
checkpatch_args: list[str],
dist_dir: pathlib.Path,
bid: str | None,
change_info: pathlib.Path | None,
bazel_wrapper: pathlib.Path,
) -> int:
if bid:
# Skip checkpatch for postsubmit (b/35390488).
if not bid.startswith("P"):
logging.info("Did not identify a presubmit build. Exiting.")
return 0
targets: list[(list[str], str, str)] = []
with change_info.open() as change_info_file:
for change in json.load(change_info_file).get("changes"):
project_name = change["project"]
project_path = pathlib.Path(change["projectPath"])
package_path = _get_package_path(project_path)
if not package_path:
logging.info("Skipping %s because it is not in the workspace.", project_path)
continue
# Only interested in the git SHA of the CL at the time of the
# build. The SHA is specified by the "latestRevision" field.
revision = change["latestRevision"]
path_targets = _find_checkpatch_targets(bazel_wrapper, package_path)
if not path_targets:
logging.info(
"Skipping %s because no checkpatch() target is found.", project_path)
continue
targets.append((path_targets, revision, project_name))
checkpatch_topdir = dist_dir / "checkpatch"
return_codes = []
for path_targets, git_sha1, project_name in targets:
sanitized_project_name = project_name.replace("/", "__")
checkpatch_dir = checkpatch_topdir / sanitized_project_name / git_sha1
checkpatch_dir.mkdir(parents=True, exist_ok=True)
checkpatch_log = checkpatch_dir / "checkpatch.log"
checkpatch_log.unlink(missing_ok=True)
checkpatch_full_log = checkpatch_dir / "checkpatch_full.log"
checkpatch_full_log.unlink(missing_ok=True)
for target in path_targets:
return_codes.append(_run_checkpatch(
bazel_wrapper=bazel_wrapper,
target=target,
git_sha1=git_sha1,
log=checkpatch_log,
checkpatch_args=checkpatch_args,
))
_run_checkpatch(
bazel_wrapper=bazel_wrapper,
target=target,
git_sha1=git_sha1,
log=checkpatch_full_log,
checkpatch_args=checkpatch_args + ["--ignored_checks", ""],
silent=True,
)
success = sum(return_codes) == 0
if not success:
logging.info(
"See %s/ folder for complete output.",
checkpatch_topdir.name
)
return 0 if success else 1
if __name__ == "__main__":
logging.basicConfig(level=_LOG_LEVEL,
format="%(levelname)s: %(message)s")
known, checkpatch_args = load_arguments()
sys.exit(main(checkpatch_args=checkpatch_args, **vars(known)))