blob: 9aadd6ccd9cfa43211afc5b5ebd2ff071d260c7c [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2024 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import json
import os
import re
import subprocess
import sys
from typing import Any, Sequence
import bazel_compdb_utils
_OPT_PATTERN = re.compile("[\W]+")
_SHOULD_LOG = False
_FUCHSIA_PACKAGE_SUFFIX = "_fuchsia_package"
def compilation_mode(optimization: str) -> str:
# sometimes the optimization is escape quoted so we clean it up.
opt = _OPT_PATTERN.sub("", optimization)
if opt == "debug":
return "--compilation_mode=dbg"
elif opt in ["size", "speed", "profile", "size_lto", "size_thinlto"]:
return "--compilation_mode=opt"
else:
return "--compilation_mode=fastbuild"
def canonicalize_label_from_arg(label: str) -> str:
# fuchsia_package targets append a suffix to them which is not obvious.
# We check the label to see if the user has appended it or not and fix
# it for them here.
if label.endswith(_FUCHSIA_PACKAGE_SUFFIX):
return label
else:
return label + _FUCHSIA_PACKAGE_SUFFIX
def assert_arg_label_is_fuchsia_package(bazel_exe: str, label: str) -> None:
results = collect_labels_from_scope(bazel_exe, label)
if len(results) == 0:
fail(
"Provided label '{}' is not a valid fuchsia_package label. Please provide a label that points to a valid fuchsia package or use --dir instead.".format(
label
)
)
def collect_labels_from_dir(args: argparse.Namespace) -> Sequence[str]:
# Clean up the scope so it matches what bazel expects.
dir = args.dir.removeprefix("//").removesuffix("...").removesuffix("/")
if dir == "":
scope = "//..."
else:
scope = "//{}/...".format(dir)
return collect_labels_from_scope(args.bazel, scope)
def collect_labels_from_scope(bazel_exe: str, scope: str) -> Sequence[str]:
try:
return bazel_compdb_utils.run(
bazel_exe,
"query",
'kind("_build_fuchsia_package(_test)? rule", {})'.format(scope),
"--ui_event_filters=-info,-warning",
"--noshow_loading_progress",
"--noshow_progress",
).splitlines()
except:
fail(
"""Unable to find any labels in {}.
This can occur when the scope is too broad and bazel tries to query
paths that are not compatible with bazel. For example, if you try to
query the root directory it will pick up the prebuilt directory which
contains files that cause the query to fail.
Try the query again with a more limited scope.
""".format(
scope
)
)
return [] # So pytype is happy.
def fail(msg: str, exit_code: int = 1) -> None:
print("ERROR: ", msg)
sys.exit(exit_code)
def info(msg: str) -> None:
if _SHOULD_LOG:
print("INFO: ", msg)
def init_logger(verbose: bool) -> None:
global _SHOULD_LOG
if verbose:
_SHOULD_LOG = True
def is_none(obj: Any) -> bool:
return obj == None
def main(argv: Sequence[str]) -> None:
parser = argparse.ArgumentParser(description="Refresh bazel compdb")
parser.add_argument("--bazel", required=True, help="The bazel binary")
parser.add_argument(
"--build-dir", required=True, help="The build directory"
)
parser.add_argument(
"--label",
help="The bazel label to query. This label must point to a fuchsia_package or one of its test variants.",
)
parser.add_argument(
"--dir",
help="""A directory to search for labels relative to //
This path must be a path that we can run `fx bazel query` on. Some paths
are not compatible with bazel queries and will fail.""",
)
parser.add_argument(
"--optimization", required=True, help="The build level optimization"
)
parser.add_argument(
"--target-cpu", required=True, help="The cpu we are targeting"
)
parser.add_argument(
"-v",
"--verbose",
required=False,
help="If we should print info logs",
default=False,
action="store_true",
)
parser.add_argument(
"--self-test-filter",
required=False,
help="""If provided will run a self-test on the files that match the filter.
The self-test will attempt to compile the file given the set of arguments
in the compile commands. This check can be very slow because it needs to
compile every file that matches the filter. It is directly invoking clang
do it does not benefit from the cached results. This flag should only be
used for debugging.
When used in conjunction with --verbose, the command will print out the
clang errors.
The filter will perform a re.search on the file.
""",
default=None,
)
args = parser.parse_args(argv)
init_logger(args.verbose)
if is_none(args.label) and is_none(args.dir):
fail("Either --label or --dir must be set.")
labels = []
if args.label:
label = canonicalize_label_from_arg(args.label)
info("Verifying label '{}' is valid".format(label))
assert_arg_label_is_fuchsia_package(args.bazel, label)
labels.append(label)
if args.dir:
info("Finding all labels in dir '{}'".format(args.dir))
labels.extend(collect_labels_from_dir(args))
new_compile_commands = bazel_compdb_utils.compdb_for_labels(
args.build_dir,
args.bazel,
compilation_mode(args.optimization),
args.target_cpu,
labels,
)
compile_commands_dict = {}
compile_commands_path = os.path.join(
args.build_dir, "compile_commands.json"
)
with open(
compile_commands_path,
"r",
) as f:
compile_commands = json.load(f)
compile_commands.extend(new_compile_commands)
for compile_command in compile_commands:
compile_commands_dict[compile_command["file"]] = compile_command
with open(
compile_commands_path,
"w",
) as f:
json.dump(list(compile_commands_dict.values()), f, indent=2)
if args.self_test_filter:
commands_to_check = [
c
for c in compile_commands
if re.search(args.self_test_filter, c["file"])
]
info("CHECKING {} commands".format(len(commands_to_check)))
info(
"SKIPPING {} commands".format(
len(compile_commands) - len(commands_to_check)
)
)
num_failures = 0
for command in commands_to_check:
if "arguments" in command:
clang_args = command["arguments"]
else:
clang_args = command["command"].split()
try:
subprocess.check_output(
clang_args,
text=True,
cwd=command["directory"],
stderr=None if args.verbose else subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
num_failures += 1
if num_failures > 0:
info(f"SELF TEST RESULTS: {num_failures} FAILURES")
sys.exit(1)
else:
info("SELF TEST PASSED WITH NO FAILURES")
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))