diff --git a/.github/scripts/verify_console_output.py b/.github/scripts/verify_console_output.py new file mode 100644 index 0000000..696daeb --- /dev/null +++ b/.github/scripts/verify_console_output.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +import argparse +import difflib +import re +import sys +from pathlib import Path + +ANSI_PATTERN = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") +LOG_PREFIX_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}T[0-9:.]+Z\s+") +SHA_PATTERN = re.compile( + r"(https://github\.com/[^/]+/[^/]+/blob/)[0-9a-f]{40}(/)" +) +REPORT_MARKER = "##[error] Issues found!" +RESULT_LABELS = ( + "cppcheck results:", + "clang-tidy results:", + "PyLint results:", +) +CPP_ISSUE_PATTERN = re.compile( + r"^(?P/.+?):(?P\d+):(?:\d+:)?\s+" + r"(?Perror|warning|style|performance|portability|information|note):\s+" + r"(?P.+)$" +) +PYTHON_ISSUE_PATTERN = re.compile( + r"^(?P.+?\.py):(?P\d+):?\s+(?P[A-Z]\d{4}:.+)$" +) + + +def _split_inline_result_label(line: str) -> list[str]: + for label in RESULT_LABELS: + if line.startswith(label): + remainder = line[len(label):].strip() + return [label, remainder] if remainder else [label] + return [line] + + +def _canonical_issue_line(line: str) -> str | None: + match = CPP_ISSUE_PATTERN.match(line) + if match: + return ( + f"{match.group('path')}:{match.group('line')}: " + f"{match.group('level')}: {match.group('message')}" + ) + + match = PYTHON_ISSUE_PATTERN.match(line) + if match: + return f"{match.group('path')}:{match.group('line')}: {match.group('message')}" + + return None + + +def normalize_console_output(output: str) -> str: + normalized = ANSI_PATTERN.sub("", output) + marker_index = normalized.rfind(REPORT_MARKER) + if marker_index == -1: + raise RuntimeError(f"Console output does not contain '{REPORT_MARKER}'") + + normalized = normalized[marker_index:] + normalized = SHA_PATTERN.sub(r"\1\2", normalized) + normalized = normalized.replace("\r\n", "\n").replace("\r", "\n") + + lines: list[str] = [] + for original_line in normalized.split("\n"): + original_line = LOG_PREFIX_PATTERN.sub("", original_line) + for line in _split_inline_result_label(original_line.strip()): + if not line: + continue + + if line == REPORT_MARKER or line in RESULT_LABELS: + lines.append(line) + continue + + issue_line = _canonical_issue_line(line) + if issue_line is not None: + lines.append(issue_line) + + return "\n".join(lines).strip() + "\n" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--log", required=True) + parser.add_argument("--fixture", required=True) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + actual = normalize_console_output(Path(args.log).read_text(encoding="utf-8")) + expected = normalize_console_output(Path(args.fixture).read_text(encoding="utf-8")) + + if actual == expected: + print(f"Console output matches fixture: {args.fixture}") + return 0 + + diff = difflib.unified_diff( + expected.splitlines(), + actual.splitlines(), + fromfile=args.fixture, + tofile=args.log, + lineterm="", + ) + print("\n".join(diff)) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/testrepo_console_fixtures/test-static-analysis-python__python.txt b/.github/testrepo_console_fixtures/test-static-analysis-python__python.txt new file mode 100644 index 0000000..feed48c --- /dev/null +++ b/.github/testrepo_console_fixtures/test-static-analysis-python__python.txt @@ -0,0 +1,8 @@ +##[error] Issues found! +PyLint results: +source.py:42: E0001: Parsing failed: 'Missing parentheses in call to 'print'. Did you mean print(...)? (source, line 42)' (syntax-error) +local.py:31: C0301: Line too long (182/100) (line-too-long) +local.py:1: C0114: Missing module docstring (missing-module-docstring) +local.py:1: C0116: Missing function or method docstring (missing-function-docstring) +local.py:1: W0621: Redefining name 'patch' from outer scope (line 29) (redefined-outer-name) +local.py:29: C0103: Constant name "patch" doesn't conform to UPPER_CASE naming style (invalid-name) diff --git a/.github/testrepo_console_fixtures/test-static-analysis__sa-cmake-output.txt b/.github/testrepo_console_fixtures/test-static-analysis__sa-cmake-output.txt new file mode 100644 index 0000000..1a7c799 --- /dev/null +++ b/.github/testrepo_console_fixtures/test-static-analysis__sa-cmake-output.txt @@ -0,0 +1,32 @@ +##[error] Issues found! +cppcheck results: +/github/workspace/source.cpp:9: warning: Class 'Example' does not have a copy constructor which is recommended since it has dynamic memory/resource allocation(s). [noCopyConstructor] +/github/workspace/source.cpp:9: warning: Class 'Example' does not have a operator= which is recommended since it has dynamic memory/resource allocation(s). [noOperatorEq] +/github/workspace/source.cpp:36: error: Null pointer dereference: ptr [nullPointer] +/github/workspace/source.cpp:35: note: Assignment 'ptr=nullptr', assigned value is 0 +/github/workspace/source.cpp:36: note: Null pointer dereference +/github/workspace/source.cpp:35: style: Variable 'ptr' can be declared as pointer to const [constVariablePointer] +/github/workspace/source.cpp:39: error: Out of bounds access in expression 'vec[10]' because 'vec' is empty. [containerOutOfBounds] +/github/workspace/source.cpp:41: style: Unused variable: unusedvec [unusedVariable] +clang-tidy results: +/github/workspace/another_source.cpp:1: error: function 'do_nothing' can be made static or moved into an anonymous namespace to enforce internal linkage [misc-use-internal-linkage,-warnings-as-errors] +/github/workspace/source.cpp:2: error: included header string is not used directly [misc-include-cleaner,-warnings-as-errors] +/github/workspace/source.cpp:5: error: class 'Example' defines a non-default destructor but does not define a copy constructor, a copy assignment operator, a move constructor or a move assignment operator [cppcoreguidelines-special-member-functions,hicpp-special-member-functions,-warnings-as-errors] +/github/workspace/source.cpp:9: error: 42 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers,-warnings-as-errors] +/github/workspace/source.cpp:11: error: parameter name 'p' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:21: error: function 'divide' can be made static or moved into an anonymous namespace to enforce internal linkage [misc-use-internal-linkage,-warnings-as-errors] +/github/workspace/source.cpp:21: error: parameter name 'a' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:21: error: parameter name 'b' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:23: error: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl,-warnings-as-errors] +/github/workspace/source.cpp:30: error: variable 'ex' of type 'Example' can be declared 'const' [misc-const-correctness,-warnings-as-errors] +/github/workspace/source.cpp:30: error: variable name 'ex' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:31: error: variable 'x' of type 'int' can be declared 'const' [misc-const-correctness,-warnings-as-errors] +/github/workspace/source.cpp:31: error: variable name 'x' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:31: error: 10 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers,-warnings-as-errors] +/github/workspace/source.cpp:32: error: variable 'y' of type 'int' can be declared 'const' [misc-const-correctness,-warnings-as-errors] +/github/workspace/source.cpp:32: error: variable name 'y' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:33: error: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl,-warnings-as-errors] +/github/workspace/source.cpp:36: error: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl,-warnings-as-errors] +/github/workspace/source.cpp:39: error: 10 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers,-warnings-as-errors] +/github/workspace/source.cpp:39: error: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl,-warnings-as-errors] +/github/workspace/source.cpp:41: error: variable 'unusedvec' of type 'std::vector' can be declared 'const' [misc-const-correctness,-warnings-as-errors] diff --git a/.github/testrepo_console_fixtures/test-static-analysis__sa-non-cmake-output.txt b/.github/testrepo_console_fixtures/test-static-analysis__sa-non-cmake-output.txt new file mode 100644 index 0000000..1a7c799 --- /dev/null +++ b/.github/testrepo_console_fixtures/test-static-analysis__sa-non-cmake-output.txt @@ -0,0 +1,32 @@ +##[error] Issues found! +cppcheck results: +/github/workspace/source.cpp:9: warning: Class 'Example' does not have a copy constructor which is recommended since it has dynamic memory/resource allocation(s). [noCopyConstructor] +/github/workspace/source.cpp:9: warning: Class 'Example' does not have a operator= which is recommended since it has dynamic memory/resource allocation(s). [noOperatorEq] +/github/workspace/source.cpp:36: error: Null pointer dereference: ptr [nullPointer] +/github/workspace/source.cpp:35: note: Assignment 'ptr=nullptr', assigned value is 0 +/github/workspace/source.cpp:36: note: Null pointer dereference +/github/workspace/source.cpp:35: style: Variable 'ptr' can be declared as pointer to const [constVariablePointer] +/github/workspace/source.cpp:39: error: Out of bounds access in expression 'vec[10]' because 'vec' is empty. [containerOutOfBounds] +/github/workspace/source.cpp:41: style: Unused variable: unusedvec [unusedVariable] +clang-tidy results: +/github/workspace/another_source.cpp:1: error: function 'do_nothing' can be made static or moved into an anonymous namespace to enforce internal linkage [misc-use-internal-linkage,-warnings-as-errors] +/github/workspace/source.cpp:2: error: included header string is not used directly [misc-include-cleaner,-warnings-as-errors] +/github/workspace/source.cpp:5: error: class 'Example' defines a non-default destructor but does not define a copy constructor, a copy assignment operator, a move constructor or a move assignment operator [cppcoreguidelines-special-member-functions,hicpp-special-member-functions,-warnings-as-errors] +/github/workspace/source.cpp:9: error: 42 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers,-warnings-as-errors] +/github/workspace/source.cpp:11: error: parameter name 'p' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:21: error: function 'divide' can be made static or moved into an anonymous namespace to enforce internal linkage [misc-use-internal-linkage,-warnings-as-errors] +/github/workspace/source.cpp:21: error: parameter name 'a' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:21: error: parameter name 'b' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:23: error: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl,-warnings-as-errors] +/github/workspace/source.cpp:30: error: variable 'ex' of type 'Example' can be declared 'const' [misc-const-correctness,-warnings-as-errors] +/github/workspace/source.cpp:30: error: variable name 'ex' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:31: error: variable 'x' of type 'int' can be declared 'const' [misc-const-correctness,-warnings-as-errors] +/github/workspace/source.cpp:31: error: variable name 'x' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:31: error: 10 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers,-warnings-as-errors] +/github/workspace/source.cpp:32: error: variable 'y' of type 'int' can be declared 'const' [misc-const-correctness,-warnings-as-errors] +/github/workspace/source.cpp:32: error: variable name 'y' is too short, expected at least 3 characters [readability-identifier-length,-warnings-as-errors] +/github/workspace/source.cpp:33: error: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl,-warnings-as-errors] +/github/workspace/source.cpp:36: error: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl,-warnings-as-errors] +/github/workspace/source.cpp:39: error: 10 is a magic number; consider replacing it with a named constant [cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers,-warnings-as-errors] +/github/workspace/source.cpp:39: error: do not use 'std::endl' with streams; use '\n' instead [performance-avoid-endl,-warnings-as-errors] +/github/workspace/source.cpp:41: error: variable 'unusedvec' of type 'std::vector' can be declared 'const' [misc-const-correctness,-warnings-as-errors] diff --git a/.github/workflows/build_static_analysis_image.yml b/.github/workflows/build_static_analysis_image.yml new file mode 100644 index 0000000..eb7b78f --- /dev/null +++ b/.github/workflows/build_static_analysis_image.yml @@ -0,0 +1,217 @@ +name: Build Static Analysis Image + +on: + push: + branches: + - master + paths: + - Dockerfile + - docker/static_analysis.dockerfile + - .github/scripts/verify_console_output.py + - .github/testrepo_console_fixtures/** + - .github/workflows/build_static_analysis_image.yml + pull_request: + paths: + - Dockerfile + - docker/static_analysis.dockerfile + - .github/scripts/verify_console_output.py + - .github/testrepo_console_fixtures/** + - .github/workflows/build_static_analysis_image.yml + workflow_dispatch: + +concurrency: + group: build-static-analysis-image-${{ github.event.pull_request.number || github.ref_name }} + cancel-in-progress: true + +jobs: + build: + name: Build and Verify Base Image + runs-on: ubuntu-24.04 + env: + DOCKERHUB_IMAGE: jdomagala/static_analysis + steps: + - uses: actions/checkout@v4 + + - name: Build image + run: | + set -euo pipefail + docker build \ + --file docker/static_analysis.dockerfile \ + --tag static-analysis-base:ci \ + . + + - name: Verify image exists + run: | + set -euo pipefail + docker image inspect static-analysis-base:ci >/dev/null + + - name: Print image size + run: | + set -euo pipefail + size_bytes="$(docker image inspect static-analysis-base:ci --format '{{.Size}}')" + size_human="$(numfmt --to=iec-i --suffix=B "$size_bytes")" + + echo "static-analysis-base:ci size: ${size_human} (${size_bytes} bytes)" + + { + echo "### Docker Image Size" + echo + echo "- \`static-analysis-base:ci\`: ${size_human} (${size_bytes} bytes)" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Build action image + run: | + set -euo pipefail + docker build \ + --build-arg BASE_IMAGE=static-analysis-base:ci \ + --tag static-analysis-action:ci \ + . + + - name: Smoke test action on TestRepo + run: | + set -euo pipefail + + test_repo="$RUNNER_TEMP/TestRepo" + fixtures_dir="$GITHUB_WORKSPACE/.github/testrepo_console_fixtures" + cppcheck_args="--enable=all --suppress=missingIncludeSystem" + cppcheck_args+=" --suppress=functionStatic --suppress=unusedFunction" + cppcheck_args+=" --inline-suppr --inconclusive" + git clone --depth 1 --branch test-static-analysis https://github.com/JacobDomagala/TestRepo.git "$test_repo" + test_sha="$(git -C "$test_repo" rev-parse HEAD)" + + cat > "$test_repo/init_script.sh" <<'EOF' + #!/bin/bash + set -euo pipefail + root_dir="${1}" + build_dir="${2}" + echo "Hello from the init script! First arg=${root_dir} second arg=${build_dir}" + EOF + + common_docker_args=( + --volume "$test_repo:/github/workspace" + --env GITHUB_WORKSPACE=/github/workspace + --env GITHUB_EVENT_NAME=push + --env GITHUB_SHA="$test_sha" + --env GITHUB_REPOSITORY=JacobDomagala/TestRepo + --env GITHUB_REF=refs/heads/test-static-analysis + --env GITHUB_REF_NAME=test-static-analysis + --env GITHUB_BASE_REF= + --env GITHUB_HEAD_REF= + --env INPUT_GITHUB_TOKEN= + --env INPUT_REPO=JacobDomagala/TestRepo + --env INPUT_PR_NUM= + --env INPUT_PR_REPO= + --env INPUT_PR_HEAD= + --env INPUT_FORCE_CONSOLE_PRINT=true + --env INPUT_VERBOSE=true + --env INPUT_REPORT_PR_CHANGES_ONLY=false + --env INPUT_APT_PCKGS= + --env INPUT_INIT_SCRIPT=init_script.sh + --env INPUT_EXCLUDE_DIR=lib + ) + + run_entrypoint() { + local label="$1" + shift + + local log_file="$RUNNER_TEMP/static-analysis-${label}.log" + echo "::group::${label}" + set +e + docker run --rm \ + --workdir /github/workspace \ + "${common_docker_args[@]}" \ + --env INPUT_COMMENT_TITLE="$label" \ + "$@" \ + static-analysis-action:ci 2>&1 | tee "$log_file" + local status="${PIPESTATUS[0]}" + set -e + echo "::endgroup::" + + if grep -q "Traceback (most recent call last)" "$log_file"; then + echo "${label} crashed with a Python traceback" + exit 1 + fi + + if ! grep -q "##\\[error\\] Issues found!" "$log_file"; then + echo "${label} did not reach the expected static-analysis findings output" + exit 1 + fi + + if [ "$status" -ge 128 ]; then + echo "${label} exited with unexpected status ${status}" + exit "$status" + fi + + echo "${label} completed entrypoint smoke test with expected findings (exit ${status})." + } + + verify_console_output() { + local log_file="$1" + local fixture="$2" + + python3 .github/scripts/verify_console_output.py \ + --log "$log_file" \ + --fixture "$fixture" + } + + run_entrypoint "SA CMake output" \ + --env INPUT_LANGUAGE=C++ \ + --env INPUT_USE_CMAKE=true \ + --env INPUT_CMAKE_ARGS=-DCMAKE_BUILD_TYPE=Debug \ + --env INPUT_COMPILE_COMMANDS= \ + --env INPUT_COMPILE_COMMANDS_REPLACE_PREFIX=false \ + --env INPUT_CLANG_TIDY_ARGS=-extra-arg=-std=c++20 \ + --env INPUT_CPPCHECK_ARGS="$cppcheck_args" + + test -f "$test_repo/build/compile_commands.json" + verify_console_output "$RUNNER_TEMP/static-analysis-SA CMake output.log" \ + "$fixtures_dir/test-static-analysis__sa-cmake-output.txt" + + run_entrypoint "SA non-CMake output" \ + --env INPUT_LANGUAGE=C++ \ + --env INPUT_USE_CMAKE=false \ + --env INPUT_CMAKE_ARGS=-DCMAKE_BUILD_TYPE=Debug \ + --env INPUT_COMPILE_COMMANDS= \ + --env INPUT_COMPILE_COMMANDS_REPLACE_PREFIX=false \ + --env INPUT_CLANG_TIDY_ARGS=-extra-arg=-std=c++20 \ + --env INPUT_CPPCHECK_ARGS="$cppcheck_args" + verify_console_output "$RUNNER_TEMP/static-analysis-SA non-CMake output.log" \ + "$fixtures_dir/test-static-analysis__sa-non-cmake-output.txt" + + run_entrypoint "Python" \ + --env INPUT_LANGUAGE=Python \ + --env INPUT_PYTHON_DIRS=. \ + --env INPUT_PYLINT_ARGS= + verify_console_output "$RUNNER_TEMP/static-analysis-Python.log" \ + "$fixtures_dir/test-static-analysis-python__python.txt" + + - name: Log in to Docker Hub + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + uses: docker/login-action@v3 + with: + username: jdomagala + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push image to Docker Hub + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + env: + GIT_SHA: ${{ github.sha }} + run: | + set -euo pipefail + + short_sha="${GIT_SHA::12}" + latest_tag="${DOCKERHUB_IMAGE}:latest" + sha_tag="${DOCKERHUB_IMAGE}:sha-${short_sha}" + + docker tag static-analysis-base:ci "$latest_tag" + docker tag static-analysis-base:ci "$sha_tag" + + docker push "$latest_tag" + docker push "$sha_tag" + + { + echo "### Published Docker Tags" + echo + echo "- \`$latest_tag\`" + echo "- \`$sha_tag\`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/Dockerfile b/Dockerfile index ad59e55..b0eaa17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM jdomagala/static_analysis:latest +ARG BASE_IMAGE=jdomagala/static_analysis:latest +FROM ${BASE_IMAGE} WORKDIR /src diff --git a/docker/static_analysis.dockerfile b/docker/static_analysis.dockerfile index 57ddc9f..e642c3f 100644 --- a/docker/static_analysis.dockerfile +++ b/docker/static_analysis.dockerfile @@ -1,40 +1,96 @@ +FROM ubuntu:24.04 AS cppcheck-builder + +ARG CPPCHECK_VERSION=2.16.0 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + cmake \ + git \ + ninja-build \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /tmp + +RUN git clone --branch "${CPPCHECK_VERSION}" --depth 1 https://github.com/danmar/cppcheck.git \ + && cmake -S /tmp/cppcheck -B /tmp/cppcheck/build -G Ninja \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DCMAKE_INSTALL_PREFIX=/opt/cppcheck \ + && cmake --build /tmp/cppcheck/build --parallel \ + && cmake --install /tmp/cppcheck/build \ + && strip /opt/cppcheck/bin/cppcheck + + +FROM ubuntu:24.04 AS python-tools-builder + +ENV DEBIAN_FRONTEND=noninteractive \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + python3 \ + python3-pip \ + && python3 -m pip install --break-system-packages --no-cache-dir --no-compile \ + --target /opt/python-tools \ + PyGithub \ + pylint \ + && find /opt/python-tools -type d -name "__pycache__" -prune -exec rm -rf {} + \ + && find /opt/python-tools -type f -name "*.pyc" -delete \ + && rm -rf /var/lib/apt/lists/* + + +FROM ubuntu:24.04 AS llvm-repo + +ARG CLANG_VERSION=20 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + gnupg \ + wget \ + && wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor -o /llvm.gpg \ + && printf "deb [signed-by=/usr/share/keyrings/llvm.gpg] http://apt.llvm.org/noble/ llvm-toolchain-noble-%s main\n" "${CLANG_VERSION}" > /llvm.list \ + && rm -rf /var/lib/apt/lists/* + + FROM ubuntu:24.04 AS base -# Define versions as environment variables -ENV CLANG_VERSION=20 \ - CPPCHECK_VERSION=2.16.0 \ - CXX=clang++ \ +ARG CLANG_VERSION=20 +ENV DEBIAN_FRONTEND=noninteractive \ + CLANG_VERSION=${CLANG_VERSION} \ CC=clang \ - DEBIAN_FRONTEND=noninteractive - -# Copy the llvm.sh installation script -COPY llvm.sh /llvm.sh - -# Install dependencies -RUN apt-get update && apt-get install -y \ - build-essential python3 python3-pip git wget libssl-dev ninja-build \ - lsb-release software-properties-common gnupg \ - # Execute the LLVM install script with the version number - && chmod +x /llvm.sh && /llvm.sh $CLANG_VERSION \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* \ - # Install Python packages - && pip3 install --break-system-packages PyGithub pylint \ - # Create symlinks for clang and clang++ - && 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 + CXX=clang++ \ + PATH="/opt/cppcheck/bin:${PATH}" \ + PYTHONPATH="/opt/python-tools" -WORKDIR /opt +COPY --from=python-tools-builder /opt/python-tools /opt/python-tools +COPY --from=cppcheck-builder /opt/cppcheck /opt/cppcheck +COPY --from=llvm-repo /llvm.gpg /usr/share/keyrings/llvm.gpg +COPY --from=llvm-repo /llvm.list /etc/apt/sources.list.d/llvm.list -# Build CMake from source -RUN git clone https://github.com/Kitware/CMake.git \ - && cd CMake \ - && ./bootstrap && make -j$(nproc) && 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 +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + cmake \ + git \ + make \ + python3 \ + clang-tidy-${CLANG_VERSION} \ + && ln -sf "/usr/bin/clang++-${CLANG_VERSION}" /usr/bin/clang++ \ + && ln -sf "/usr/bin/clang-${CLANG_VERSION}" /usr/bin/clang \ + && ln -sf "/usr/bin/clang-tidy-${CLANG_VERSION}" /usr/bin/clang-tidy \ + && ln -sf "/usr/bin/run-clang-tidy-${CLANG_VERSION}" /usr/bin/run-clang-tidy \ + && ln -sf /usr/bin/python3 /usr/bin/python \ + && printf '%s\n' '#!/bin/sh' 'exec python3 -m pylint "$@"' > /usr/local/bin/pylint \ + && chmod +x /usr/local/bin/pylint \ + && rm -rf \ + /var/lib/apt/lists/* \ + /usr/share/doc/* \ + /usr/share/man/* \ + /usr/share/locale/* + +WORKDIR /opt diff --git a/entrypoint_cpp.sh b/entrypoint_cpp.sh index 3af337d..35f1d8d 100644 --- a/entrypoint_cpp.sh +++ b/entrypoint_cpp.sh @@ -11,6 +11,12 @@ common_ancestor=${common_ancestor:-""} CLANG_TIDY_ARGS="${INPUT_CLANG_TIDY_ARGS//$'\n'/}" CPPCHECK_ARGS="${INPUT_CPPCHECK_ARGS//$'\n'/}" +RUN_CLANG_TIDY_BIN="${RUN_CLANG_TIDY_BIN:-$(command -v run-clang-tidy || command -v "run-clang-tidy-${CLANG_VERSION:-20}" || compgen -c | grep '^run-clang-tidy-[0-9]\+$' | head -n 1 || true)}" + +if [ -z "$RUN_CLANG_TIDY_BIN" ]; then + debug_print "Error: run-clang-tidy executable not found in PATH." + exit 1 +fi if [ -n "$INPUT_COMPILE_COMMANDS" ]; then debug_print "Using compile_commands.json file ($INPUT_COMPILE_COMMANDS) - use_cmake input is not being used!" @@ -94,16 +100,16 @@ else cat cppcheck_*.txt > cppcheck.txt # Excludes for clang-tidy are handled in python script - debug_print "Running run-clang-tidy-20 $CLANG_TIDY_ARGS -p $compile_commands_dir $files_to_check >>clang_tidy.txt 2>&1" - eval run-clang-tidy-20 "$CLANG_TIDY_ARGS" -p "$compile_commands_dir" "$files_to_check" > clang_tidy.txt 2>&1 || true + debug_print "Running $RUN_CLANG_TIDY_BIN $CLANG_TIDY_ARGS -p $compile_commands_dir $files_to_check >>clang_tidy.txt 2>&1" + eval "$RUN_CLANG_TIDY_BIN" "$CLANG_TIDY_ARGS" -p "$compile_commands_dir" "$files_to_check" > clang_tidy.txt 2>&1 || true else # Without compile_commands.json debug_print "Running cppcheck -j $num_proc $files_to_check $CPPCHECK_ARGS --output-file=cppcheck.txt ..." eval cppcheck -j "$num_proc" "$files_to_check" "$CPPCHECK_ARGS" --output-file=cppcheck.txt || true - debug_print "Running run-clang-tidy-20 $CLANG_TIDY_ARGS $files_to_check >>clang_tidy.txt 2>&1" - eval run-clang-tidy-20 "$CLANG_TIDY_ARGS" "$files_to_check" > clang_tidy.txt 2>&1 || true + debug_print "Running $RUN_CLANG_TIDY_BIN $CLANG_TIDY_ARGS $files_to_check >>clang_tidy.txt 2>&1" + eval "$RUN_CLANG_TIDY_BIN" "$CLANG_TIDY_ARGS" "$files_to_check" > clang_tidy.txt 2>&1 || true fi cd /