[libcamera-ci,RFC,v1] Run clang-tidy
diff mbox series

Message ID 20261015101012.89244-1-barnabas.pocze@ideasonboard.com
State New
Headers show
Series
  • [libcamera-ci,RFC,v1] Run clang-tidy
Related show

Commit Message

Barnabás Pőcze Oct. 15, 2026, 10:10 a.m. UTC
Add a job that runs clang-tidy, based on a clang build to avoid issues
arising from unsupported compiler options between gcc and clang.

Unfortunately running clang-tidy on each source file for every change
is too time consuming, so only the affected files should be checked.
However, knowing which files are affected does not appear to be
particularly simple. The biggest challenge is posed by generated header
files, changing any of the inputs should trigger a clang-tidy run on
every source file that includes it.

To handle that, both the "static" inputs of a target, and the compiler
generated depfiles are needed. Unfortunately, ninja does not seem to
have a subcommand, that when given a list of "changed" files, would
print the list of targets that need to be potentially rebuilt.

So this change only implements the handling of the compiler generated
depfiles using the `deps` ninja subtool. This will miss generated
headers, but an implemention handling that seems too complex at the
moment. In practice, this means, for example, that changing e.g. only
`include/libcamera/formats.h.in` will not trigger a clang-tidy run
on any source file that includes it.

Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
---
I am planning to enable more checks in the libcamera repository,
but given the limitations outlined above, I wanted to see if it
is acceptable in the first place.

Example job: https://gitlab.freedesktop.org/pobrn/libcamera/-/jobs/112824160
---
 .gitlab-ci/lint-clang-tidy.py | 112 ++++++++++++++++++++++++++++++++++
 .gitlab-ci/lint-clang-tidy.sh |  22 +++++++
 .gitlab-ci/setup-container.sh |   2 +-
 gitlab-ci.yml                 |  34 ++++++++++-
 4 files changed, 167 insertions(+), 3 deletions(-)
 create mode 100755 .gitlab-ci/lint-clang-tidy.py
 create mode 100755 .gitlab-ci/lint-clang-tidy.sh

Patch
diff mbox series

diff --git a/.gitlab-ci/lint-clang-tidy.py b/.gitlab-ci/lint-clang-tidy.py
new file mode 100755
index 0000000..aae28db
--- /dev/null
+++ b/.gitlab-ci/lint-clang-tidy.py
@@ -0,0 +1,112 @@ 
+#!/usr/bin/env python3
+
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: © 2026 Ideas On Board Oy
+
+import argparse
+import fnmatch
+import json
+import os
+import os.path
+import subprocess
+
+def main():
+	parser = argparse.ArgumentParser()
+	parser.add_argument("--build-dir", type=str, required=True)
+	parser.add_argument("--range", nargs=2, required=False)
+	parser.add_argument("args", nargs="*", default=[])
+
+	args = parser.parse_args()
+
+	try:
+		exclude_patterns = []
+		with open(".clang-tidy-exclude", "r") as f:
+			for line in f:
+				line = line.strip()
+				if not line or line.startswith("#"):
+					continue
+
+				exclude_patterns.append(line)
+	except FileNotFoundError:
+		pass
+
+	# extract source files from the compilation database
+	r = subprocess.run(
+		[ "ninja", "-t", "compdb" ],
+		cwd=args.build_dir,
+		stdin=subprocess.DEVNULL,
+		stdout=subprocess.PIPE,
+		check=True,
+	)
+
+	source_files = set()
+
+	for x in json.loads(r.stdout):
+		f = os.path.abspath(os.path.join(x["directory"], x["file"]))
+		if not any(fnmatch.fnmatch(f, p) for p in exclude_patterns):
+			source_files.add(f)
+
+	print("sources files:", len(source_files))
+
+	if args.range:
+		# extract changed files from git
+		r = subprocess.run(
+			[ "git", "diff", "--name-only", args.range[0], args.range[1] ],
+			stdin=subprocess.DEVNULL,
+			stdout=subprocess.PIPE,
+			check=True,
+		)
+		changed_files = {
+			os.path.realpath(x.strip())
+			for x in r.stdout.decode("utf-8").splitlines()
+		}
+
+		print(f"changed files: ({len(changed_files)})")
+		for x in changed_files:
+			print(f"\t{x}")
+
+		# extract changed dependencies
+		r = subprocess.run(
+			[ "ninja", "-t", "deps" ],
+			cwd=args.build_dir,
+			stdin=subprocess.DEVNULL,
+			stdout=subprocess.PIPE,
+			check=True,
+		)
+
+		dependency_groups = []
+		dependency_group = set()
+		for line in r.stdout.decode("utf-8").splitlines():
+			if not line.startswith("    "):
+				if bool(changed_files & dependency_group):
+					dependency_groups.append(dependency_group)
+
+				dependency_group = set()
+			else:
+				dependency_group.add(
+					os.path.abspath(os.path.join(args.build_dir, line.strip()))
+				)
+
+		# select source files that are in changed dependency groups
+		files_to_check = set()
+		for f in source_files:
+			if any(f in dg for dg in dependency_groups):
+				files_to_check.add(f)
+	else:
+		files_to_check = source_files
+
+	print(f"running clang-tidy on the following files: ({len(files_to_check)})")
+	for f in files_to_check:
+		print(f"\t{f}")
+
+	if files_to_check:
+		argv = [
+			"run-clang-tidy",
+			"-p", args.build_dir
+		] + args.args
+
+		print("executing:", argv, flush=True)
+		os.execvp(argv[0], argv + list(files_to_check))
+
+if __name__ == "__main__":
+	main()
diff --git a/.gitlab-ci/lint-clang-tidy.sh b/.gitlab-ci/lint-clang-tidy.sh
new file mode 100755
index 0000000..4c88795
--- /dev/null
+++ b/.gitlab-ci/lint-clang-tidy.sh
@@ -0,0 +1,22 @@ 
+#!/bin/bash
+
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: © 2026 Ideas On Board Oy
+#
+# Lint with clang-tidy
+
+set -e
+
+source "$(dirname "$0")/lib.sh"
+
+clang_tidy() {
+	local base=$(find_base origin/$CI_DEFAULT_BRANCH $CI_COMMIT_SHA)
+
+	$(dirname "$0")/lint-clang-tidy.py \
+		--build-dir build/ \
+		--range $base $CI_COMMIT_SHA \
+		-- \
+		-j $BUILD_CONCURRENT_JOBS
+}
+
+run clang_tidy
diff --git a/.gitlab-ci/setup-container.sh b/.gitlab-ci/setup-container.sh
index bb00895..ac37faf 100755
--- a/.gitlab-ci/setup-container.sh
+++ b/.gitlab-ci/setup-container.sh
@@ -132,7 +132,7 @@  case $FDO_DISTRIBUTION_VERSION in
 	# Packages required to deploy the documentation
 	PKGS_LIBCAMERA_RUNTIME+=( openssh-client rsync )
 	# Tools requires by the lint jobs.
-	PKGS_LIBCAMERA_RUNTIME+=( clang-format jq python3-autopep8 reuse shellcheck )
+	PKGS_LIBCAMERA_RUNTIME+=( clang-format clang-tidy jq python3-autopep8 reuse shellcheck )
 	# libclang-rt-dev for the clang ASan runtime.
 	PKGS_LIBCAMERA_RUNTIME_MULTIARCH+=( libclang-rt-19-dev )
 	# For lc-compliance in the `test-lc-compliance:virtual` job
diff --git a/gitlab-ci.yml b/gitlab-ci.yml
index 840eb74..3e8fe99 100644
--- a/gitlab-ci.yml
+++ b/gitlab-ci.yml
@@ -74,7 +74,7 @@  include:
 .libcamera-ci.debian:13:
   variables:
     FDO_DISTRIBUTION_VERSION: 'trixie'
-    FDO_DISTRIBUTION_TAG: '2026-06-13.0'
+    FDO_DISTRIBUTION_TAG: '2026-10-14.0'
 
 .container-debian:
   extends:
@@ -198,9 +198,11 @@  container-arch:
     paths:
       - build/meson-logs/
       - build/build.ninja
+      - $EXTRA_ARTIFACT
   variables:
     BUILD_TYPE: debug
     MESON_OPTIONS: ${MESON_ALL_OPTIONS}
+    EXTRA_ARTIFACT: ''
 
 build-full:debian:12:
   extends:
@@ -255,6 +257,13 @@  build-full:debian:13:
           ${MESON_ALL_OPTIONS}
           -D b_sanitize=none
           -D cpp_debugstl=false
+      - ARCH: amd64
+        CC: clang
+        CXX: clang++
+        MESON_OPTIONS: >-
+          ${MESON_ALL_OPTIONS}
+          ${MESON_CLANG_LIBSTDCPP_OPTIONS}
+        EXTRA_ARTIFACT: build
       - ARCH: amd64
         CC: clang
         CXX: clang++
@@ -262,7 +271,6 @@  build-full:debian:13:
           # `cpp_debugstl=false` needed approximately between meson 1.4.0 and 1.9.3
           # with clang 18 or later due to https://github.com/mesonbuild/meson/issues/13812
           - ${MESON_ALL_OPTIONS} ${MESON_CLANG_LIBCPP_OPTIONS} -D qcam=disabled -D cpp_debugstl=false
-          - ${MESON_ALL_OPTIONS} ${MESON_CLANG_LIBSTDCPP_OPTIONS}
       - ARCH: amd64
         BUILD_TYPE: release
         CC: clang
@@ -449,6 +457,28 @@  build-docs:
 # Lint stage - Run checkstyle.py and check merge suitability
 # ------------------------------------------------------------------------------
 
+clang-tidy:
+  extends:
+    - .fdo.distribution-image@debian
+    - .history-jobs
+    - .libcamera-ci.debian:13
+    - .libcamera-ci.scripts
+  stage: lint
+  needs:
+    - job: build-full:debian:13
+      artifacts: true
+      parallel:
+        matrix:
+          - ARCH: amd64
+            CC: clang
+            CXX: clang++
+            MESON_OPTIONS: >-
+              ${MESON_ALL_OPTIONS}
+              ${MESON_CLANG_LIBSTDCPP_OPTIONS}
+            EXTRA_ARTIFACT: build
+  script:
+    $CI_PROJECT_DIR/.gitlab-ci/lint-clang-tidy.sh
+
 license:
   extends:
     - .fdo.distribution-image@debian