Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ py_binary(

# Build & Test script template
exports_files(
["codechecker_script.py"],
[
"codechecker_script.py",
"code_checker_script.py",
],
)

# The following are flags and default values for clang_tidy_aspect
Expand Down
156 changes: 88 additions & 68 deletions src/code_checker.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,19 @@
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")

CODE_CHECKER_WRAPPER_SCRIPT = """#!/usr/bin/env bash
#set -x
DATA_DIR=$1
shift
CLANG_TIDY_PLIST=$1
shift
CLANGSA_PLIST=$1
shift
LOG_FILE=$1
shift
COMPILE_COMMANDS_JSON=$1
shift
COMPILE_COMMANDS_ABS=$COMPILE_COMMANDS_JSON.abs
sed 's|"directory":"."|"directory":"'$(pwd)'"|g' $COMPILE_COMMANDS_JSON > $COMPILE_COMMANDS_ABS
echo "CodeChecker command: $@" $COMPILE_COMMANDS_ABS > $LOG_FILE
echo "===-----------------------------------------------------===" >> $LOG_FILE
echo " CodeChecker error log " >> $LOG_FILE
echo "===-----------------------------------------------------===" >> $LOG_FILE
eval "$@" $COMPILE_COMMANDS_ABS >> $LOG_FILE 2>&1
# ls -la $DATA_DIR
# NOTE: the following we do to get rid of md5 hash in plist file names
ret_code=$?
echo "===-----------------------------------------------------===" >> $LOG_FILE
if [ $ret_code -eq 1 ] || [ $ret_code -ge 128 ]; then
echo "===-----------------------------------------------------==="
echo "[ERROR]: CodeChecker returned with $ret_code!"
cat $LOG_FILE
exit 1
fi
cp $DATA_DIR/*_clang-tidy_*.plist $CLANG_TIDY_PLIST
cp $DATA_DIR/*_clangsa_*.plist $CLANGSA_PLIST

# sed -i -e "s|<string>.*execroot/bazel_codechecker/|<string>|g" $CLANG_TIDY_PLIST
# sed -i -e "s|<string>.*execroot/bazel_codechecker/|<string>|g" $CLANGSA_PLIST

"""
def _get_analyzers(options):
pattern = r"--analyzers"
analyzers_raw_string = ""
for s in options:
if s.startswith(pattern):
analyzers_raw_string = s
break
if analyzers_raw_string == "":
return []
analyzers = []
analyzers_raw_string = analyzers_raw_string.removeprefix("--analyzers")
analyzers = analyzers_raw_string.strip("= ").split(" ")
return analyzers

def _run_code_checker(
ctx,
Expand All @@ -53,47 +30,53 @@ def _run_code_checker(
# Define Plist and log file names
data_dir = ctx.attr.name + "/data"
file_name_params = (data_dir, src.path.replace("/", "-"))
clang_tidy_plist_file_name = "{}/{}_clang-tidy.plist".format(*file_name_params)
clangsa_plist_file_name = "{}/{}_clangsa.plist".format(*file_name_params)
codechecker_log_file_name = "{}/{}_codechecker.log".format(*file_name_params)

# Declare output files
clang_tidy_plist = ctx.actions.declare_file(clang_tidy_plist_file_name)
clangsa_plist = ctx.actions.declare_file(clangsa_plist_file_name)
codechecker_log = ctx.actions.declare_file(codechecker_log_file_name)

inputs = [compile_commands_json] + sources_and_headers
outputs = [clang_tidy_plist, clangsa_plist, codechecker_log]

# Create CodeChecker wrapper script
wrapper = ctx.actions.declare_file(ctx.attr.name + "/code_checker.sh")
ctx.actions.write(
output = wrapper,
is_executable = True,
content = CODE_CHECKER_WRAPPER_SCRIPT,
)

# Prepare arguments
args = ctx.actions.args()

# NOTE: we pass: data dir, PList and log file names as first 4 arguments
args.add(data_dir)
args.add(clang_tidy_plist.path)
args.add(clangsa_plist.path)
args.add(codechecker_log.path)
args.add(compile_commands_json.path)
args.add("CodeChecker")
args.add("analyze")
args.add_all(options)
args.add("--output=" + data_dir)
args.add("--file=*/" + src.path)
outputs = [codechecker_log]

analyzers = _get_analyzers(options)
analyzer_output_paths = "" # List of tuples (analyzer_name, plist_path)
if "clangsa" in analyzers:
clangsa_plist_file_name = "{}/{}_clangsa.plist".format(*file_name_params)
clangsa_plist = ctx.actions.declare_file(clangsa_plist_file_name)
analyzer_output_paths += "clangsa," + clangsa_plist.path + ";"
outputs.append(clangsa_plist)
if "clang-tidy" in analyzers:
clang_tidy_plist_file_name = "{}/{}_clang-tidy.plist".format(*file_name_params)
clang_tidy_plist = ctx.actions.declare_file(clang_tidy_plist_file_name)
analyzer_output_paths += "clang-tidy," + clang_tidy_plist.path + ";"
outputs.append(clang_tidy_plist)
if "cppcheck" in analyzers:
cppcheck_plist_file_name = "{}/{}_cppcheck.plist".format(*file_name_params)
cppcheck_plist = ctx.actions.declare_file(cppcheck_plist_file_name)
analyzer_output_paths += "cppcheck," + cppcheck_plist.path + ";"
outputs.append(cppcheck_plist)
if "gcc" in analyzers:
gcc_plist_file_name = "{}/{}_gcc.plist".format(*file_name_params)
gcc_plist = ctx.actions.declare_file(gcc_plist_file_name)
analyzer_output_paths += "gcc," + gcc_plist.path + ";"
outputs.append(gcc_plist)
if "infer" in analyzers:
infer_plist_file_name = "{}/{}_infer.plist".format(*file_name_params)
infer_plist = ctx.actions.declare_file(infer_plist_file_name)
analyzer_output_paths += "infer," + infer_plist.path + ";"
outputs.append(infer_plist)

# Action to run CodeChecker for a file
ctx.actions.run(
inputs = inputs,
outputs = outputs,
executable = wrapper,
arguments = [args],
executable = ctx.outputs.code_checker_script,
arguments = [
data_dir,
src.path,
codechecker_log.path,
analyzer_output_paths
],
mnemonic = "CodeChecker",
use_default_shell_env = True,
progress_message = "CodeChecker analyze {}".format(src.short_path),
Expand All @@ -119,7 +102,6 @@ def check_valid_file_type(src):
return False

def _rule_sources(ctx):

srcs = []
if hasattr(ctx.rule.attr, "srcs"):
for src in ctx.rule.attr.srcs:
Expand Down Expand Up @@ -290,11 +272,41 @@ def _collect_all_sources_and_headers(ctx):
sources_and_headers = all_files + headers.to_list()
return sources_and_headers

def _merge_options(default, custom):
"""
Merge command line arguments so that default options can be overridden
"""
final = []
args_set = []
for item in custom:
args_set.append(item.split(" ")[0].split("=")[0])
final.append(item)
for option in default:
if option.split(" ")[0].split("=")[0] not in args_set:
final.append(option)
return final

def _create_wrapper_script(ctx, options, compile_commands_json):
options_str = ""
for item in options:
options_str += item + " "
ctx.actions.expand_template(
template = ctx.file._code_checker_script_template,
output = ctx.outputs.code_checker_script,
is_executable = True,
substitutions = {
"{PythonPath}": ctx.attr._python_runtime[PyRuntimeInfo].interpreter_path,
"{compile_commands_json}": compile_commands_json.path,
"{codechecker_args}": options_str,
},
)

def _code_checker_impl(ctx):
compile_commands_json = _compile_commands_impl(ctx)
sources_and_headers = _collect_all_sources_and_headers(ctx)
options = ctx.attr.default_options + ctx.attr.options
options = _merge_options(ctx.attr.default_options, ctx.attr.options)
all_files = [compile_commands_json]
_create_wrapper_script(ctx, options, compile_commands_json)
for target in ctx.attr.targets:
if not CcInfo in target:
continue
Expand Down Expand Up @@ -360,9 +372,17 @@ code_checker_test = rule(
],
doc = "List of compilable targets which should be checked.",
),
"_code_checker_script_template": attr.label(
default = ":code_checker_script.py",
allow_single_file = True,
),
"_python_runtime": attr.label(
default = "@default_python_tools//:py3_runtime",
),
},
outputs = {
"test_script": "%{name}/test_script.sh",
"code_checker_script": "%{name}/code_checker_script.py",
},
test = True,
)
143 changes: 143 additions & 0 deletions src/code_checker_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!{PythonPath}

# Copyright 2023 Ericsson AB
#
# 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.


import os
import re
import shutil
import subprocess
import sys

DATA_DIR: str = sys.argv[1]
FILE_PATH: str = sys.argv[2]
ANALYZER_PLIST_PATHS: list[list[str]] = [
item.split(",") for item in sys.argv[4].split(";")
]
LOG_FILE: str = sys.argv[3]
COMPILE_COMMANDS_JSON: str = "{compile_commands_json}"
COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs"
CODECHECKER_ARGS: str = "{codechecker_args}"


def log(msg: str) -> None:
"""
Append message to the log file
"""
with open(LOG_FILE, "a") as log_file:
log_file.write(msg)


def _create_compile_commands_json_with_absolute_paths():
"""
Modifies the paths in compile_commands.json to contain the absolute path
of the files.
"""
with open(COMPILE_COMMANDS_JSON, "r") as original_file, open(
COMPILE_COMMANDS_ABSOLUTE, "w"
) as new_file:
content = original_file.read()
# Replace '"directory":"."' with the absolute path
# of the current working directory
new_content = content.replace(
'"directory":".', f'"directory":"{os.getcwd()}'
)
new_file.write(new_content)


def _run_codechecker() -> None:
"""
Runs CodeChecker analyze
"""
log(
f"CodeChecker command: CodeChecker analyze {CODECHECKER_ARGS} \
{COMPILE_COMMANDS_ABSOLUTE} --output={DATA_DIR} --file=*/{FILE_PATH}\n"
)
log("===-----------------------------------------------------===\n")
log(" CodeChecker error log \n")
log("===-----------------------------------------------------===\n")

result = subprocess.run(
["echo", "$PATH"],
shell=True,
env=os.environ,
capture_output=True,
text=True,
)
log(result.stdout)

codechecker_cmd: list[str] = (
["CodeChecker", "analyze"]
+ CODECHECKER_ARGS.split()
+ ["--output=" + DATA_DIR]
+ ["--file=*/" + FILE_PATH]
+ [COMPILE_COMMANDS_ABSOLUTE]
)

try:
with open(LOG_FILE, "a") as log_file:
subprocess.run(
codechecker_cmd,
env=os.environ,
stdout=log_file,
stderr=log_file,
check=True,
)
except subprocess.CalledProcessError as e:
log(e.output.decode() if e.output else "")
if e.returncode == 1 or e.returncode >= 128:
_display_error(e.returncode)


def _display_error(ret_code: int) -> None:
"""
Display the log file, and exit with 1
"""
# Log and exit on error
print("===-----------------------------------------------------===")
print(f"[ERROR]: CodeChecker returned with {ret_code}!")
with open(LOG_FILE, "r") as log_file:
print(log_file.read())
sys.exit(1)


def _move_plist_files():
"""
Move the plist files from the temporary directory to their final destination
"""
# NOTE: the following we do to get rid of md5 hash in plist file names
# Copy the plist files to the specified destinations
for file in os.listdir(DATA_DIR):
for analyzer_info in ANALYZER_PLIST_PATHS:
if re.search(
rf"_{analyzer_info[0]}_.*\.plist$", file
) and os.path.isfile(os.path.join(DATA_DIR, file)):
shutil.copy(os.path.join(DATA_DIR, file), analyzer_info[1])


def main():
_create_compile_commands_json_with_absolute_paths()
_run_codechecker()
_move_plist_files()


if __name__ == "__main__":
main()


# I have conserved this comment from the original bash script
# The sed commands are commented out, so we won't implement them
# # sed -i -e "s|<string>.*execroot/bazel_codechecker/|<string>|g" $CLANG_TIDY_PLIST
# # sed -i -e "s|<string>.*execroot/bazel_codechecker/|<string>|g" $CLANGSA_PLIST