diff --git a/docker/static_analysis.dockerfile b/docker/static_analysis.dockerfile index 12b9463..dd36a7f 100644 --- a/docker/static_analysis.dockerfile +++ b/docker/static_analysis.dockerfile @@ -1,34 +1,30 @@ -FROM ubuntu:22.10 as base +FROM ubuntu:23.04 as base +# Define versions as environment variables +ENV CLANG_VERSION=16 +ENV CPPCHECK_VERSION=2.12.0 + +# Other environment variables ENV CXX=clang++ ENV CC=clang - ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y build-essential \ - python3 python3-pip git clang-15 clang-tidy-15 wget libssl-dev ninja-build && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* -RUN pip3 install PyGithub - -RUN ln -s \ - "$(which clang++-15)" \ - /usr/bin/clang++ - -RUN ln -s \ - "$(which clang-15)" \ - /usr/bin/clang - -RUN ln -s \ - /usr/bin/python3 \ - /usr/bin/python - -RUN git clone https://github.com/Kitware/CMake.git && \ - cd CMake && ./bootstrap && \ - make -j4 && make install - -RUN wget 'https://sourceforge.net/projects/cppcheck/files/cppcheck/2.9/cppcheck-2.9.tar.gz/download' && \ - tar xf download && \ - cd cppcheck-2.9 && mkdir build && cd build && \ - cmake -G "Ninja" .. && ninja install - +# Install dependencies +RUN apt-get update && apt-get install -y \ + build-essential python3 python3-pip git clang-$CLANG_VERSION clang-tidy-$CLANG_VERSION wget libssl-dev ninja-build \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && pip3 install PyGithub --break-system-packages \ + && ln -s "$(which clang++-$CLANG_VERSION)" /usr/bin/clang++ \ + && ln -s "$(which clang-$CLANG_VERSION)" /usr/bin/clang \ + && ln -s /usr/bin/python3 /usr/bin/python + +WORKDIR /opt + +# Build CMake from source +RUN git clone https://github.com/Kitware/CMake.git \ + && cd CMake && ./bootstrap && make -j4 && make install + +# Install cppcheck +RUN git clone https://github.com/danmar/cppcheck.git \ + && cd cppcheck && git checkout tags/$CPPCHECK_VERSION && mkdir build && cd build && cmake -G Ninja .. && ninja all && ninja install diff --git a/entrypoint.sh b/entrypoint.sh index 85ad79c..850995d 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -53,6 +53,16 @@ if [ "$GITHUB_EVENT_NAME" = "pull_request_target" ] && [ -n "$INPUT_PR_REPO" ]; export GITHUB_WORKSPACE=$(pwd) fi +preselected_files="" +if [ "$INPUT_REPORT_PR_CHANGES_ONLY" = true ]; then + echo "The 'report_pr_changes_only' option is enabled. Running SA only on modified files." + git config --global --add safe.directory /github/workspace + git fetch origin + common_ancestor=$(git merge-base origin/"$GITHUB_BASE_REF" "origin/$GITHUB_HEAD_REF") + preselected_files="$(git diff --name-only "$common_ancestor" "origin/$GITHUB_HEAD_REF" | grep -E '\.(c|cpp|h|hpp)$')" + debug_print "Preselected files: \n$preselected_files" +fi + debug_print "GITHUB_WORKSPACE = ${GITHUB_WORKSPACE} INPUT_EXCLUDE_DIR = ${INPUT_EXCLUDE_DIR} use_extra_directory = ${use_extra_directory}" mkdir -p build @@ -67,15 +77,18 @@ fi cd build if [ "$INPUT_USE_CMAKE" = true ]; then - cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON "$INPUT_CMAKE_ARGS" .. + # Trim trailing newlines + INPUT_CMAKE_ARGS="${INPUT_CMAKE_ARGS%"${INPUT_CMAKE_ARGS##*[![:space:]]}"}" + debug_print "Running cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON $INPUT_CMAKE_ARGS -S $GITHUB_WORKSPACE -B $(pwd)" + eval "cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON $INPUT_CMAKE_ARGS -S $GITHUB_WORKSPACE -B $(pwd)" fi if [ -z "$INPUT_EXCLUDE_DIR" ]; then - files_to_check=$(python3 /get_files_to_check.py -dir="$GITHUB_WORKSPACE") - debug_print "Running: files_to_check=python3 /get_files_to_check.py -dir=\"$GITHUB_WORKSPACE\")" + files_to_check=$(python3 /get_files_to_check.py -dir="$GITHUB_WORKSPACE" -preselected="$preselected_files") + debug_print "Running: files_to_check=python3 /get_files_to_check.py -dir=\"$GITHUB_WORKSPACE\" -preselected=\"$preselected_files\")" else - files_to_check=$(python3 /get_files_to_check.py -exclude="$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR" -dir="$GITHUB_WORKSPACE") - debug_print "Running: files_to_check=python3 /get_files_to_check.py -exclude=\"$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR\" -dir=\"$GITHUB_WORKSPACE\")" + files_to_check=$(python3 /get_files_to_check.py -exclude="$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR" -dir="$GITHUB_WORKSPACE" -preselected="$preselected_files") + debug_print "Running: files_to_check=python3 /get_files_to_check.py -exclude=\"$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR\" -dir=\"$GITHUB_WORKSPACE\" -preselected=\"$preselected_files\")" fi debug_print "Files to check = $files_to_check" @@ -90,14 +103,18 @@ if [ "$INPUT_USE_CMAKE" = true ]; then debug_print "Running cppcheck --project=compile_commands.json $INPUT_CPPCHECK_ARGS --output-file=cppcheck.txt -i$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR ..." eval cppcheck --project=compile_commands.json "$INPUT_CPPCHECK_ARGS" --output-file=cppcheck.txt -i"$GITHUB_WORKSPACE/$INPUT_EXCLUDE_DIR" "$GITHUB_WORKSPACE" || true fi + + # Excludes for clang-tidy are handled in python script + debug_print "Running run-clang-tidy-16 $INPUT_CLANG_TIDY_ARGS -p $(pwd) $files_to_check >>clang_tidy.txt 2>&1" + eval run-clang-tidy-16 "$INPUT_CLANG_TIDY_ARGS" -p "$(pwd)" "$files_to_check" >clang_tidy.txt 2>&1 || true + else # Excludes for clang-tidy are handled in python script debug_print "Running cppcheck $files_to_check $INPUT_CPPCHECK_ARGS --output-file=cppcheck.txt ..." eval cppcheck "$files_to_check" "$INPUT_CPPCHECK_ARGS" --output-file=cppcheck.txt || true -fi -# Excludes for clang-tidy are handled in python script -debug_print "Running run-clang-tidy-15 $INPUT_CLANG_TIDY_ARGS -p $(pwd) $files_to_check >>clang_tidy.txt 2>&1" -eval run-clang-tidy-15 "$INPUT_CLANG_TIDY_ARGS" -p "$(pwd)" "$files_to_check" >clang_tidy.txt 2>&1 || true + debug_print "Running run-clang-tidy-16 $INPUT_CLANG_TIDY_ARGS $files_to_check >>clang_tidy.txt 2>&1" + eval run-clang-tidy-16 "$INPUT_CLANG_TIDY_ARGS" "$files_to_check" >clang_tidy.txt 2>&1 || true +fi python3 /run_static_analysis.py -cc cppcheck.txt -ct clang_tidy.txt -o "$print_to_console" -fk "$use_extra_directory" diff --git a/src/get_files_to_check.py b/src/get_files_to_check.py index d4af089..3cf8f45 100644 --- a/src/get_files_to_check.py +++ b/src/get_files_to_check.py @@ -2,7 +2,7 @@ from pathlib import Path -def get_files_to_check(directory_in, excludes_in): +def get_files_to_check(directory_in, excludes_in, preselected_files): """ Given a directory path and a string of prefixes to exclude, return a space-separated string of all files in the directory (and its subdirectories) @@ -11,6 +11,7 @@ def get_files_to_check(directory_in, excludes_in): Args: directory_in (str): The path to the directory to search for files. excludes_in (str): A space-separated string of prefixes to exclude from the search. + preselected_files (str): If present, then it's the list of files to be checked (minus excluded ones) Returns: str: A space-separated string of file paths that meet the search criteria. @@ -26,12 +27,19 @@ def get_files_to_check(directory_in, excludes_in): supported_extensions = (".h", ".hpp", ".hcc", ".c", ".cc", ".cpp", ".cxx") all_files = [] - for path in Path(directory_in).rglob("*.*"): - path_ = str(path.resolve()) - if path_.endswith(supported_extensions) and not path_.startswith( - tuple(exclude_prefixes) - ): - all_files.append(path_) + if len(preselected_files) == 0: + for path in Path(directory_in).rglob("*.*"): + path_ = str(path.resolve()) + if path_.endswith(supported_extensions) and not path_.startswith( + tuple(exclude_prefixes) + ): + all_files.append(path_) + else: + for file in preselected_files: + if not file.startswith(directory_in): + file = f"{directory_in}/{file}" + if not file.startswith(tuple(exclude_prefixes)): + all_files.append(file) return " ".join(all_files) @@ -39,9 +47,13 @@ def get_files_to_check(directory_in, excludes_in): if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("-exclude", help="Exclude prefix", required=False) - parser.add_argument("-dir", help="Current directory", required=True) + parser.add_argument( + "-preselected", help="Preselected files", default="", required=False + ) + parser.add_argument("-dir", help="Source directory", required=True) directory = parser.parse_args().dir + preselected = parser.parse_args().preselected excludes = parser.parse_args().exclude - print(get_files_to_check(directory, excludes)) + print(get_files_to_check(directory, excludes, preselected.split())) diff --git a/test/test_static_analysis.py b/test/test_static_analysis.py index a0926ea..991d4a5 100644 --- a/test/test_static_analysis.py +++ b/test/test_static_analysis.py @@ -185,7 +185,7 @@ def test_get_files_to_check(self): f"{pwd}/utils/dummy_project/exclude_dir_2/ExcludedFile2.hpp", ] result = get_files_to_check.get_files_to_check( - f"{pwd}/utils/dummy_project", None + f"{pwd}/utils/dummy_project", None, "" ) self.assertEqual(to_list_and_sort(result), expected) @@ -197,7 +197,7 @@ def test_get_files_to_check(self): f"{pwd}/utils/dummy_project/exclude_dir_2/ExcludedFile2.hpp", ] result = get_files_to_check.get_files_to_check( - f"{pwd}/utils/dummy_project", f"{pwd}/utils/dummy_project/exclude_dir_1" + f"{pwd}/utils/dummy_project", f"{pwd}/utils/dummy_project/exclude_dir_1", "" ) self.assertEqual(to_list_and_sort(result), expected) @@ -210,6 +210,15 @@ def test_get_files_to_check(self): result = get_files_to_check.get_files_to_check( f"{pwd}/utils/dummy_project", f"{pwd}/utils/dummy_project/exclude_dir_1 {pwd}/utils/dummy_project/exclude_dir_2", + "", + ) + + # Preselected files present + expected = [f"{pwd}/utils/dummy_project/DummyFile.cpp"] + result = get_files_to_check.get_files_to_check( + f"{pwd}/utils/dummy_project", + f"{pwd}/utils/dummy_project/exclude_dir_1 {pwd}/utils/dummy_project/exclude_dir_2", + f"{pwd}/utils/dummy_project/DummyFile.cpp {pwd}/utils/dummy_project/exclude_dir_1/ExcludedFile1.hpp", )