Files
scst/scripts/lint_kbuild.py
Gleb Chesnokov 6c3405f5fe scripts: Add Cppcheck with native build contexts
Expose make cppcheck using freshly captured kernel CHECK commands and
real userspace compiler invocations. Keep their include paths and
definitions unchanged and analyze kernel and userspace contexts
separately.

Use the shared source scope without additional input exclusions, project
models or diagnostic suppressions. Relevant diffs trigger full
configured analysis; KUnit coverage follows the actual build
configuration.

Retain build logs, compilation databases, native XML and checker
coverage. Separate source findings from parser failures, missing inputs
and incomplete builds, with isolated report and capture fixtures.
2026-10-19 21:38:56 +03:00

90 lines
4.0 KiB
Python

"""Serialize analyzer builds and retain evidence of native CHECK invocations."""
from contextlib import contextmanager
from collections import Counter
import fcntl
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
import sys
import tempfile
from lint_common import run
from sparse_smatch.native import checker_command
from lint_scope import in_scope, tracked_sources
@contextmanager
def build_lock(root):
path = root / ".cache/lint/kbuild.lock"
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
yield
def validate_checker(root, tool, log):
"""Fail before a module build if the selected kernel rejects the checker."""
checker_command(tool) # Require the executable and native data even on old kernels.
kernel = Path(os.environ.get("SCST_LINT_KDIR") or os.environ.get("KDIR") or
f"/lib/modules/{os.environ.get('KVER') or os.uname().release}/build")
if not (kernel / "Makefile").is_file():
raise ValueError(f"kernel build tree not found: {kernel}")
probe = kernel / "scripts/checker-valid.sh"
if not probe.exists():
return # Older supported kernels have no capability probe.
command = [sys.executable, str(root / "scripts/sparse_smatch/check.py"), tool]
# checker-valid.sh filters native output and can lose the native status in
# a pipeline. Record both independently, just like ordinary CHECK steps.
with tempfile.TemporaryDirectory(prefix="scst-checker-probe-") as directory:
record = Path(directory) / "probe.json"
record.write_text(json.dumps({"returncode": 2}))
env = dict(os.environ, SCST_LINT_PROBE_RECORD=str(record), SCST_LINT_PROBE_LOG=str(log))
result = run([str(probe), *command, *shlex.split(os.environ.get("CHECKFLAGS", ""))],
cwd=root, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
native_status = json.loads(record.read_text())["returncode"]
with log.open("ab") as output:
output.write(f"Kbuild checker validation: {probe}\n".encode())
output.write(result.stdout)
output.write(f"Native probe exit status: {native_status}\n".encode())
if result.returncode or native_status or result.stdout.strip() != b"1":
raise ValueError(f"{tool} is incompatible with {probe}; module build was not started")
def collect(root, tool, directory, log, *, checker=None):
"""Retain CHECK exits even when the enclosing Kbuild discards them."""
env = dict(os.environ, SCST_LINT_RECORDS=str(directory))
checker = shlex.join(checker or
[sys.executable, str(root / "scripts/sparse_smatch/check.py"), tool])
command = ["make", "--no-silent", "-k", "-C", str(root), "lint-kbuild",
"C=2", "V=0", "CHECK=" + checker]
with log.open("ab") as output:
result = run(command, env=env, stdout=output, stderr=subprocess.STDOUT,
progress=f"{tool.capitalize()} Kbuild")
records = [json.loads(path.read_text()) for path in sorted(Path(directory).glob("*.json"))]
return result.returncode, records
def unchecked_invocations(root, tool, log, records):
"""Detect a CHECK whose adapter never managed to leave a record."""
checked = Counter(Path(row["file"]) for row in records)
sources = set(tracked_sources(root, tool)) | set(checked)
announced = Counter()
for line in log.read_text(errors="replace").splitlines():
match = re.fullmatch(r"\s+CHECK\s+(?:\[M\]\s+)?(.+\.c)\s*", line)
if not match:
continue
path = Path(match[1])
if path.is_absolute():
candidates = {path.resolve()} if in_scope(path, root, tool) else set()
else:
candidates = {source for source in sources
if source.as_posix().endswith("/" + path.as_posix())}
if len(candidates) > 1:
raise ValueError(f"ambiguous Kbuild CHECK path: {path}")
announced.update(candidates)
return sorted(map(str, (announced - checked).elements()))