build: implement clang-tidy using our plugin

The principle of this is that you can either externally build it with
Nix (actual implementation will be in a future commit), or it can be
built with meson if the Nix one is not passed in.

The idea I have is that dev shells don't receive the one from Nix to
avoid having to build it, but CI can use the one from Nix and save some
gratuitous rebuilds.

The design of this is that you can run `ninja -C build clang-tidy` and
it will simply correctly clang-tidy the codebase in spite of PCH
bullshit caused by the cc-wrapper.

This is a truly horrendous number of hacks in a ball, caused by bugs in
several pieces of software, and I am not even getting started.

I don't consider this to fix the clang-tidy issue filing, since we still
have a fair number of issues to fix even on the existing minimal
configuration, and I have not yet implemented it in CI. Realistically we
will need to do something like https://github.com/Ericsson/codechecker
to be able to silence warnings without physically touching the code, or
at least *diff* reports between versions.

Also, the run-clang-tidy output design is rather atrocious and must
not be inflicted upon anyone I have respect for, since it buries the
diagnostics in a pile of invocation logs. We would do really well to
integrate with the Gerrit SARIF stuff so we can dump the reports on
people in a user-friendly manner.

Related: https://git.lix.systems/lix-project/lix/issues/147

Change-Id: Ifefe533f3b56874795de231667046b2da6ff2461
This commit is contained in:
Jade Lovelace 2024-07-31 20:37:27 -07:00
parent 32ca194ebf
commit 3daeeaefb1
21 changed files with 269 additions and 15 deletions

View file

@ -1,13 +0,0 @@
project('lix-clang-tidy', ['cpp', 'c'],
version : '0.1',
default_options : ['warning_level=3', 'cpp_std=c++20'])
llvm = dependency('Clang', version: '>= 14', modules: ['libclang'])
sources = files(
'HasPrefixSuffix.cc',
'LixClangTidyChecks.cc',
'FixIncludes.cc',
)
shared_module('lix-clang-tidy', sources,
dependencies: llvm)

View file

@ -0,0 +1,10 @@
---
synopsis: "clang-tidy support"
cls: 1697
issues: fj#147
credits: jade
category: Development
---
`clang-tidy` can be used to lint Lix with a limited set of lints using `ninja -C build clang-tidy` and `ninja -C build clang-tidy-fix`.
In practice, this fixes the built-in meson rule that was used the same as above being broken ever since precompiled headers were introduced.

View file

@ -197,6 +197,8 @@
busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell; busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell;
}; };
lix-clang-tidy = final.callPackage ./subprojects/lix-clang-tidy { };
# Export the patched version of boehmgc that Lix uses into the overlay # Export the patched version of boehmgc that Lix uses into the overlay
# for consumers of this flake. # for consumers of this flake.
boehmgc-nix = final.nix.passthru.boehmgc-nix; boehmgc-nix = final.nix.passthru.boehmgc-nix;
@ -384,6 +386,8 @@
rec { rec {
inherit (nixpkgsFor.${system}.native) nix; inherit (nixpkgsFor.${system}.native) nix;
default = nix; default = nix;
inherit (nixpkgsFor.${system}.native) lix-clang-tidy;
} }
// ( // (
lib.optionalAttrs (builtins.elem system linux64BitSystems) { lib.optionalAttrs (builtins.elem system linux64BitSystems) {

View file

@ -25,3 +25,13 @@ install *OPTIONS: (build OPTIONS)
# Run tests # Run tests
test *OPTIONS: test *OPTIONS:
meson test -C build --print-errorlogs {{ OPTIONS }} meson test -C build --print-errorlogs {{ OPTIONS }}
alias clang-tidy := lint
lint:
ninja -C build clang-tidy
alias clang-tidy-fix := lint-fix
lint-fix:
ninja -C build clang-tidy-fix

View file

@ -548,3 +548,5 @@ if enable_tests
subdir('tests/unit') subdir('tests/unit')
subdir('tests/functional') subdir('tests/functional')
endif endif
subdir('meson/clang-tidy')

View file

@ -1,7 +1,7 @@
# vim: filetype=meson # vim: filetype=meson
option('enable-build', type : 'boolean', value : true, option('enable-build', type : 'boolean', value : true,
description : 'Set to false to not actually build. Only really makes sense with -Dinternal-api-docs=true', description : 'set to false to not actually build. Only really makes sense with -Dinternal-api-docs=true',
) )
option('gc', type : 'feature', option('gc', type : 'feature',
@ -37,7 +37,7 @@ option('tests-brief', type : 'boolean', value : false,
) )
option('profile-build', type : 'feature', value: 'disabled', option('profile-build', type : 'feature', value: 'disabled',
description : 'whether to enable -ftime-trace in clang builds, allowing for speeding up the build.' description : 'whether to enable -ftime-trace in clang builds, allowing for diagnosing the cause of build time.'
) )
option('store-dir', type : 'string', value : '/nix/store', option('store-dir', type : 'string', value : '/nix/store',
@ -68,3 +68,7 @@ option('profile-dir', type : 'string', value : 'etc/profile.d',
option('enable-pch-std', type : 'boolean', value : true, option('enable-pch-std', type : 'boolean', value : true,
description : 'whether to use precompiled headers for C++\'s standard library (breaks clangd if you\'re using GCC)', description : 'whether to use precompiled headers for C++\'s standard library (breaks clangd if you\'re using GCC)',
) )
option('lix-clang-tidy-checks-path', type : 'string', value : '',
description: 'path to lix-clang-tidy-checks library file, if providing it externally. Uses an internal one if this is not set',
)

View file

@ -0,0 +1,21 @@
#!/usr/bin/env python3
import subprocess
def get_targets_of_rule(build_root: str, rule_name: str) -> list[str]:
return subprocess.check_output(['ninja', '-C', build_root, '-t', 'targets', 'rule', rule_name]).decode().strip().splitlines()
def ninja_build(build_root: str, targets: list[str]):
subprocess.check_call(['ninja', '-C', build_root, '--', *targets])
def main():
import argparse
ap = argparse.ArgumentParser(description='Builds required targets for clang-tidy')
ap.add_argument('build_root', help='Ninja build root', type=str)
args = ap.parse_args()
targets = [t for t in get_targets_of_rule(args.build_root, 'CUSTOM_COMMAND') if t.endswith('gen.hh')]
ninja_build(args.build_root, targets)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,53 @@
#!/usr/bin/env python3
# Deletes the PCH arguments from a compilation database, to workaround nixpkgs
# stdenv having a cc-wrapper that is impossible to use for anything except cc
# itself, for example, clang-tidy.
import json
import shlex
def process_compdb(compdb: list[dict]) -> list[dict]:
def munch_command(args: list[str]) -> list[str]:
out = []
eat_next = False
for i, arg in enumerate(args):
if arg == '-fpch-preprocess':
# as used with gcc
continue
elif arg == '-include-pch' or (arg == '-include' and args[i + 1] == 'precompiled-headers.hh'):
# -include-pch some-pch (clang), or -include some-pch (gcc)
eat_next = True
continue
if not eat_next:
out.append(arg)
eat_next = False
return out
def chomp(item: dict) -> dict:
item = item.copy()
item['command'] = shlex.join(munch_command(shlex.split(item['command'])))
return item
return [chomp(x) for x in compdb if not x['file'].endswith('precompiled-headers.hh')]
def main():
import argparse
ap = argparse.ArgumentParser(
description='Delete pch arguments from compilation database')
ap.add_argument('input',
type=argparse.FileType('r'),
help='Input json file')
ap.add_argument('output',
type=argparse.FileType('w'),
help='Output json file')
args = ap.parse_args()
input_json = json.load(args.input)
json.dump(process_compdb(input_json), args.output, indent=2)
if __name__ == '__main__':
main()

0
meson/clang-tidy/fake.cc Normal file
View file

View file

@ -0,0 +1,98 @@
# The clang-tidy target for Lix
run_clang_tidy = find_program('run-clang-tidy', required : false)
# Although this looks like it wants to be pkg-config, pkg-config does not
# really work for *plugins*, which are executable-like .so files that also
# cannot be found via find_program. Fun!
if get_option('lix-clang-tidy-checks-path') != ''
lix_clang_tidy_so = get_option('lix-clang-tidy-checks-path')
lix_clang_tidy_so_found = true
else
lix_clang_tidy_subproj = subproject(
'lix-clang-tidy',
required : false,
default_options : {'build-by-default': false}
)
if lix_clang_tidy_subproj.found()
lix_clang_tidy_so = lix_clang_tidy_subproj.get_variable('lix_clang_tidy')
lix_clang_tidy_so_found = true
else
lix_clang_tidy_so_found = false
endif
endif
# Due to numerous problems, such as:
# - Meson does not expose pch targets, but *fine*, I can just ask Ninja for
# them with `ninja -t targets rule cpp_PCH` and build them manually:
# https://github.com/mesonbuild/meson/issues/13499
# - Nixpkgs stdenv buries the cc-wrapper under a giant pile of assumptions
# about the cc-wrapper actually being used on the cc of a stdenv, rather than
# independently for clang-tidy, and we need to use cc-wrapper to get the
# correct hardening flags so that clang-tidy can actually parse the PCH file
#
# I give up. I am going to delete the damn PCH args and then it will work.
meson.add_postconf_script(
python,
meson.current_source_dir() / 'clean_compdb.py',
meson.global_build_root() / 'compile_commands.json',
meson.current_build_dir() / 'compile_commands.json',
)
# Horrible hack to get around not being able to depend on another target's
# generated headers in any way in the meson DSL
# https://github.com/mesonbuild/meson/issues/12817 which was incorrectly
# closed, if you *actually* need to generate the files once.
# Also related: https://github.com/mesonbuild/meson/issues/3667
#
# Or we could ban meson generators because their design is broken.
build_all_generated_headers = custom_target(
command : [
python,
meson.current_source_dir() / 'build_required_targets.py',
meson.global_build_root(),
],
output : 'generated_headers.stamp',
build_by_default : false,
build_always_stale : true,
)
if lix_clang_tidy_so_found
run_clang_tidy_args = [
'-load',
lix_clang_tidy_so,
'-p',
# We have to workaround a run-clang-tidy bug too, so we must give the
# directory name rather than the actual compdb file.
# https://github.com/llvm/llvm-project/issues/101440
meson.current_build_dir(),
'-quiet',
]
run_target(
'clang-tidy',
command : [
# XXX: This explicitly invokes it with python because of a nixpkgs bug
# where clang-unwrapped does not patch interpreters in run-clang-tidy.
# However, making clang-unwrapped depend on python is also silly, so idk.
python,
run_clang_tidy,
run_clang_tidy_args,
'-warnings-as-errors',
'*',
],
depends : [
build_all_generated_headers,
],
)
run_target(
'clang-tidy-fix',
command : [
python,
run_clang_tidy,
run_clang_tidy_args,
'-fix',
],
depends : [
build_all_generated_headers,
],
)
endif

View file

@ -0,0 +1,44 @@
{
lib,
stdenv,
cmake,
meson,
ninja,
llvmPackages,
}:
let
inherit (lib) fileset;
in
stdenv.mkDerivation {
pname = "lix-clang-tidy-checks";
# Setting the version to the Lix version is just going to cause pointless
# rebuilds due to versionSuffix and similar, and I cannot conceive of a usage
# where we actually care about its version since this is internal-only.
version = "0.1";
src = fileset.toSource {
root = ./.;
fileset = fileset.unions [
./meson.build
./meson.options
(fileset.fileFilter (
{ hasExt, ... }:
builtins.any hasExt [
"cc"
"hh"
]
) ./.)
];
};
nativeBuildInputs = [
meson
cmake
ninja
];
buildInputs = [
llvmPackages.llvm
llvmPackages.clang-unwrapped.dev
];
}

View file

@ -0,0 +1,18 @@
project('lix-clang-tidy', ['cpp', 'c'],
version : '0.1',
default_options : ['warning_level=3', 'cpp_std=c++20']
)
llvm = dependency('Clang', version: '>= 17', modules: ['libclang'])
sources = files(
'HasPrefixSuffix.cc',
'LixClangTidyChecks.cc',
'FixIncludes.cc',
)
lix_clang_tidy = shared_module('lix-clang-tidy', sources,
dependencies: llvm,
# overrides build_by_default, see https://github.com/mesonbuild/meson/issues/13498
install : get_option('build-by-default'),
build_by_default : get_option('build-by-default')
)

View file

@ -0,0 +1,3 @@
option('build-by-default', type : 'boolean', value : true,
description : 'set to false to not actually build targets by default. This is a hack to deal with meson lacking a build_by_default default option and building subprojects by default'
)