Skip to content

Commit 87e30ab

Browse files
furtibSzelethus
andauthored
Rewrite code_checker bash wrapper in python (#81)
Why: The inline bash script is very limiting. What: Rewrote the bash inline script into a separate Python file. Adresses: - --------- Co-authored-by: Kristóf Umann <[email protected]>
1 parent ff061da commit 87e30ab

File tree

3 files changed

+204
-63
lines changed

3 files changed

+204
-63
lines changed

src/BUILD

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ py_binary(
77

88
# Build & Test script template
99
exports_files(
10-
["codechecker_script.py"],
10+
[
11+
"codechecker_script.py",
12+
"per_file_script.py",
13+
],
1114
)
1215

1316
# The following are flags and default values for clang_tidy_aspect

src/per_file.bzl

Lines changed: 33 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,6 @@
44
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
55
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
66

7-
CODE_CHECKER_WRAPPER_SCRIPT = """#!/usr/bin/env bash
8-
#set -x
9-
DATA_DIR=$1
10-
shift
11-
CLANG_TIDY_PLIST=$1
12-
shift
13-
CLANGSA_PLIST=$1
14-
shift
15-
LOG_FILE=$1
16-
shift
17-
COMPILE_COMMANDS_JSON=$1
18-
shift
19-
COMPILE_COMMANDS_ABS=$COMPILE_COMMANDS_JSON.abs
20-
sed 's|"directory":"."|"directory":"'$(pwd)'"|g' $COMPILE_COMMANDS_JSON > $COMPILE_COMMANDS_ABS
21-
echo "CodeChecker command: $@" $COMPILE_COMMANDS_ABS > $LOG_FILE
22-
echo "===-----------------------------------------------------===" >> $LOG_FILE
23-
echo " CodeChecker error log " >> $LOG_FILE
24-
echo "===-----------------------------------------------------===" >> $LOG_FILE
25-
eval "$@" $COMPILE_COMMANDS_ABS >> $LOG_FILE 2>&1
26-
# ls -la $DATA_DIR
27-
# NOTE: the following we do to get rid of md5 hash in plist file names
28-
ret_code=$?
29-
echo "===-----------------------------------------------------===" >> $LOG_FILE
30-
if [ $ret_code -eq 1 ] || [ $ret_code -ge 128 ]; then
31-
echo "===-----------------------------------------------------==="
32-
echo "[ERROR]: CodeChecker returned with $ret_code!"
33-
cat $LOG_FILE
34-
exit 1
35-
fi
36-
cp $DATA_DIR/*_clang-tidy_*.plist $CLANG_TIDY_PLIST
37-
cp $DATA_DIR/*_clangsa_*.plist $CLANGSA_PLIST
38-
39-
# sed -i -e "s|<string>.*execroot/bazel_codechecker/|<string>|g" $CLANG_TIDY_PLIST
40-
# sed -i -e "s|<string>.*execroot/bazel_codechecker/|<string>|g" $CLANGSA_PLIST
41-
42-
"""
43-
447
def _run_code_checker(
458
ctx,
469
src,
@@ -71,35 +34,20 @@ def _run_code_checker(
7134

7235
outputs = [clang_tidy_plist, clangsa_plist, codechecker_log]
7336

74-
# Create CodeChecker wrapper script
75-
wrapper = ctx.actions.declare_file(ctx.attr.name + "/code_checker.sh")
76-
ctx.actions.write(
77-
output = wrapper,
78-
is_executable = True,
79-
content = CODE_CHECKER_WRAPPER_SCRIPT,
80-
)
81-
82-
# Prepare arguments
83-
args = ctx.actions.args()
84-
85-
# NOTE: we pass: data dir, PList and log file names as first 4 arguments
86-
args.add(data_dir)
87-
args.add(clang_tidy_plist.path)
88-
args.add(clangsa_plist.path)
89-
args.add(codechecker_log.path)
90-
args.add(compile_commands_json.path)
91-
args.add("CodeChecker")
92-
args.add("analyze")
93-
args.add_all(options)
94-
args.add("--output=" + data_dir)
95-
args.add("--file=*/" + src.path)
37+
analyzer_output_paths = "clangsa," + clangsa_plist.path + \
38+
";clang-tidy," + clang_tidy_plist.path
9639

9740
# Action to run CodeChecker for a file
9841
ctx.actions.run(
9942
inputs = inputs,
10043
outputs = outputs,
101-
executable = wrapper,
102-
arguments = [args],
44+
executable = ctx.outputs.per_file_script,
45+
arguments = [
46+
data_dir,
47+
src.path,
48+
codechecker_log.path,
49+
analyzer_output_paths
50+
],
10351
mnemonic = "CodeChecker",
10452
use_default_shell_env = True,
10553
progress_message = "CodeChecker analyze {}".format(src.short_path),
@@ -125,7 +73,6 @@ def check_valid_file_type(src):
12573
return False
12674

12775
def _rule_sources(ctx):
128-
12976
srcs = []
13077
if hasattr(ctx.rule.attr, "srcs"):
13178
for src in ctx.rule.attr.srcs:
@@ -314,11 +261,27 @@ def _collect_all_sources_and_headers(ctx):
314261
sources_and_headers = all_files + headers.to_list()
315262
return sources_and_headers
316263

264+
def _create_wrapper_script(ctx, options, compile_commands_json):
265+
options_str = ""
266+
for item in options:
267+
options_str += item + " "
268+
ctx.actions.expand_template(
269+
template = ctx.file._per_file_script_template,
270+
output = ctx.outputs.per_file_script,
271+
is_executable = True,
272+
substitutions = {
273+
"{PythonPath}": ctx.attr._python_runtime[PyRuntimeInfo].interpreter_path,
274+
"{compile_commands_json}": compile_commands_json.path,
275+
"{codechecker_args}": options_str,
276+
},
277+
)
278+
317279
def _per_file_impl(ctx):
318280
compile_commands_json = _compile_commands_impl(ctx)
319281
sources_and_headers = _collect_all_sources_and_headers(ctx)
320282
options = ctx.attr.default_options + ctx.attr.options
321283
all_files = [compile_commands_json]
284+
_create_wrapper_script(ctx, options, compile_commands_json)
322285
for target in ctx.attr.targets:
323286
if not CcInfo in target:
324287
continue
@@ -384,9 +347,17 @@ per_file_test = rule(
384347
],
385348
doc = "List of compilable targets which should be checked.",
386349
),
350+
"_per_file_script_template": attr.label(
351+
default = ":per_file_script.py",
352+
allow_single_file = True,
353+
),
354+
"_python_runtime": attr.label(
355+
default = "@default_python_tools//:py3_runtime",
356+
),
387357
},
388358
outputs = {
389359
"test_script": "%{name}/test_script.sh",
360+
"per_file_script": "%{name}/per_file_script.py",
390361
},
391362
test = True,
392363
)

src/per_file_script.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!{PythonPath}
2+
3+
# Copyright 2023 Ericsson AB
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
18+
import os
19+
import re
20+
import shutil
21+
import subprocess
22+
import sys
23+
from typing import Optional
24+
25+
# The output directory for CodeChecker
26+
DATA_DIR: Optional[str] = None
27+
# The file to be analyzed
28+
FILE_PATH: Optional[str] = None
29+
# List of pairs of analyzers and their plist files
30+
ANALYZER_PLIST_PATHS: Optional[list[list[str]]] = None
31+
LOG_FILE: Optional[str] = None
32+
COMPILE_COMMANDS_JSON: str = "{compile_commands_json}"
33+
COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs"
34+
CODECHECKER_ARGS: str = "{codechecker_args}"
35+
36+
37+
def parse_args():
38+
"""
39+
Parse arguments that may change from file-to-file.
40+
"""
41+
if len(sys.argv) != 5:
42+
print("Wrong amount of arguments")
43+
sys.exit(1)
44+
45+
global DATA_DIR
46+
global FILE_PATH
47+
global LOG_FILE
48+
global ANALYZER_PLIST_PATHS
49+
50+
DATA_DIR = sys.argv[1]
51+
FILE_PATH = sys.argv[2]
52+
LOG_FILE = sys.argv[3]
53+
ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[4].split(";")]
54+
55+
56+
def log(msg: str) -> None:
57+
"""
58+
Append message to the log file
59+
"""
60+
with open(LOG_FILE, "a") as log_file: # type: ignore
61+
log_file.write(msg)
62+
63+
64+
def _create_compile_commands_json_with_absolute_paths():
65+
"""
66+
Modifies the paths in compile_commands.json to contain the absolute path
67+
of the files.
68+
"""
69+
with open(COMPILE_COMMANDS_JSON, "r") as original_file, open(
70+
COMPILE_COMMANDS_ABSOLUTE, "w"
71+
) as new_file:
72+
content = original_file.read()
73+
# Replace "directory":"." with the absolute path
74+
# of the current working directory
75+
new_content = content.replace(
76+
'"directory":".', f'"directory":"{os.getcwd()}'
77+
)
78+
new_file.write(new_content)
79+
80+
81+
def _run_codechecker() -> None:
82+
"""
83+
Runs CodeChecker analyze
84+
"""
85+
log(
86+
f"CodeChecker command: CodeChecker analyze {CODECHECKER_ARGS} \
87+
{COMPILE_COMMANDS_ABSOLUTE} --output={DATA_DIR} --file=*/{FILE_PATH}\n"
88+
)
89+
log("===-----------------------------------------------------===\n")
90+
log(" CodeChecker error log \n")
91+
log("===-----------------------------------------------------===\n")
92+
93+
result = subprocess.run(
94+
["echo", "$PATH"],
95+
shell=True,
96+
env=os.environ,
97+
capture_output=True,
98+
text=True,
99+
)
100+
log(result.stdout)
101+
102+
codechecker_cmd: list[str] = (
103+
["CodeChecker", "analyze"]
104+
+ CODECHECKER_ARGS.split()
105+
+ ["--output=" + DATA_DIR] # type: ignore
106+
+ ["--file=*/" + FILE_PATH] # type: ignore
107+
+ [COMPILE_COMMANDS_ABSOLUTE]
108+
)
109+
110+
try:
111+
with open(LOG_FILE, "a") as log_file: # type: ignore
112+
subprocess.run(
113+
codechecker_cmd,
114+
env=os.environ,
115+
stdout=log_file,
116+
stderr=log_file,
117+
check=True,
118+
)
119+
except subprocess.CalledProcessError as e:
120+
log(e.output.decode() if e.output else "")
121+
if e.returncode == 1 or e.returncode >= 128:
122+
_display_error(e.returncode)
123+
124+
125+
def _display_error(ret_code: int) -> None:
126+
"""
127+
Display the log file, and exit with 1
128+
"""
129+
# Log and exit on error
130+
print("===-----------------------------------------------------===")
131+
print(f"[ERROR]: CodeChecker returned with {ret_code}!")
132+
with open(LOG_FILE, "r") as log_file: # type: ignore
133+
print(log_file.read())
134+
sys.exit(1)
135+
136+
137+
def _move_plist_files():
138+
"""
139+
Move the plist files from the temporary directory to their final destination
140+
"""
141+
# NOTE: the following we do to get rid of md5 hash in plist file names
142+
# Copy the plist files to the specified destinations
143+
for file in os.listdir(DATA_DIR):
144+
for analyzer_info in ANALYZER_PLIST_PATHS: # type: ignore
145+
if re.search(
146+
rf"_{analyzer_info[0]}_.*\.plist$", file
147+
) and os.path.isfile(
148+
os.path.join(DATA_DIR, file)
149+
): # type: ignore
150+
shutil.move(os.path.join(DATA_DIR, file), analyzer_info[1]) # type: ignore
151+
152+
153+
def main():
154+
parse_args()
155+
_create_compile_commands_json_with_absolute_paths()
156+
_run_codechecker()
157+
_move_plist_files()
158+
159+
160+
if __name__ == "__main__":
161+
main()
162+
163+
164+
# I have conserved this comment from the original bash script
165+
# The sed commands are commented out, so we won't implement them
166+
# # sed -i -e "s|<string>.*execroot/bazel_codechecker/|<string>|g" $CLANG_TIDY_PLIST
167+
# # sed -i -e "s|<string>.*execroot/bazel_codechecker/|<string>|g" $CLANGSA_PLIST

0 commit comments

Comments
 (0)