From 27c567de5d1807ac72708ea48018a21f0c6b8dd2 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Mon, 18 Nov 2024 11:20:56 +0100 Subject: [PATCH] scripts: add "clippy" internal tool Similar to the "ninja scan-build" target for C, add a clippy internal tool that runs clippy-driver on all crates in the project. The approach used is more efficient than with "ninja scan-build", and does not require rerunning Meson in a separate build directory; it uses the introspection data to find the compiler arguments for the target and invokes clippy-driver with a slightly modified command line. This could actually be applied to scan-build as well, reusing the run_tool_on_targets() function. Signed-off-by: Paolo Bonzini --- mesonbuild/compilers/rust.py | 49 +++++++++++++++++++++++++ mesonbuild/scripts/clippy.py | 67 ++++++++++++++++++++++++++++++++++ mesonbuild/scripts/run_tool.py | 10 +++++ 3 files changed, 126 insertions(+) create mode 100644 mesonbuild/scripts/clippy.py diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py index 02ac593842ad..717d5635f842 100644 --- a/mesonbuild/compilers/rust.py +++ b/mesonbuild/compilers/rust.py @@ -33,6 +33,35 @@ 's': ['-C', 'opt-level=s'], } +def get_rustup_run_and_args(exelist: T.List[str]) -> T.Optional[T.Tuple[T.List[str], T.List[str]]]: + """Given the command for a rustc executable, check if it is invoked via + "rustup run" and if so separate the "rustup [OPTIONS] run TOOLCHAIN" + part from the arguments to rustc. If the returned value is not None, + other tools (for example clippy-driver or rustdoc) can be run by placing + the name of the tool between the two elements of the tuple.""" + e = iter(exelist) + try: + if os.path.basename(next(e)) != 'rustup': + return None + # minimum three strings: "rustup run TOOLCHAIN" + n = 3 + opt = next(e) + + # options come first + while opt.startswith('-'): + n += 1 + opt = next(e) + + # then "run TOOLCHAIN" + if opt != 'run': + return None + + next(e) + next(e) + return exelist[:n], list(e) + except StopIteration: + return None + class RustCompiler(Compiler): # rustc doesn't invoke the compiler itself, it doesn't need a LINKER_PREFIX @@ -65,6 +94,7 @@ def __init__(self, exelist: T.List[str], version: str, for_machine: MachineChoic super().__init__([], exelist, version, for_machine, info, is_cross=is_cross, full_version=full_version, linker=linker) + self.rustup_run_and_args: T.Optional[T.Tuple[T.List[str], T.List[str]]] = get_rustup_run_and_args(exelist) self.base_options.update({OptionKey(o) for o in ['b_colorout', 'b_ndebug']}) if 'link' in self.linker.id: self.base_options.add(OptionKey('b_vscrt')) @@ -252,6 +282,25 @@ def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: action = "no" if disable else "yes" return ['-C', f'debug-assertions={action}', '-C', 'overflow-checks=no'] + def get_rust_tool(self, name: str, env: Environment) -> T.List[str]: + if self.rustup_run_and_args: + rustup_exelist, args = self.rustup_run_and_args + # do not use extend so that exelist is copied + exelist = rustup_exelist + [name] + else: + exelist = [name] + args = self.exelist[1:] + + from ..programs import find_external_program + for prog in find_external_program(env, self.for_machine, exelist[0], exelist[0], + [exelist[0]], allow_default_for_cross=False): + exelist[0] = prog.path + break + else: + return [] + + return exelist + args + class ClippyRustCompiler(RustCompiler): diff --git a/mesonbuild/scripts/clippy.py b/mesonbuild/scripts/clippy.py new file mode 100644 index 000000000000..a5161462cb35 --- /dev/null +++ b/mesonbuild/scripts/clippy.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2024 The Meson development team + +from __future__ import annotations +from collections import defaultdict +import os +import tempfile +import typing as T + +from .run_tool import run_tool_on_targets, run_with_buffered_output +from .. import build, mlog +from ..mesonlib import MachineChoice, PerMachine + +if T.TYPE_CHECKING: + from ..compilers.rust import RustCompiler + +class ClippyDriver: + def __init__(self, build: build.Build, tempdir: str): + self.tools: PerMachine[T.List[str]] = PerMachine([], []) + self.warned: T.DefaultDict[str, bool] = defaultdict(lambda: False) + self.tempdir = tempdir + for machine in MachineChoice: + compilers = build.environment.coredata.compilers[machine] + if 'rust' in compilers: + compiler = T.cast('RustCompiler', compilers['rust']) + self.tools[machine] = compiler.get_rust_tool('clippy-driver', build.environment) + + def warn_missing_clippy(self, machine: str) -> None: + if self.warned[machine]: + return + mlog.warning(f'clippy-driver not found for {machine} machine') + self.warned[machine] = True + + def __call__(self, target: T.Dict[str, T.Any]) -> T.Iterable[T.Coroutine[None, None, int]]: + for src_block in target['target_sources']: + if src_block['language'] == 'rust': + clippy = getattr(self.tools, src_block['machine']) + if not clippy: + self.warn_missing_clippy(src_block['machine']) + continue + + cmdlist = list(clippy) + prev = None + for arg in src_block['parameters']: + if prev: + prev = None + continue + elif arg in {'--emit', '--out-dir'}: + prev = arg + else: + cmdlist.append(arg) + + cmdlist.extend(src_block['sources']) + # the default for --emit is to go all the way to linking, + # and --emit dep-info= is not enough for clippy to do + # enough analysis, so use --emit metadata. + cmdlist.append('--emit') + cmdlist.append('metadata') + cmdlist.append('--out-dir') + cmdlist.append(self.tempdir) + yield run_with_buffered_output(cmdlist) + +def run(args: T.List[str]) -> int: + os.chdir(args[0]) + build_data = build.load(os.getcwd()) + with tempfile.TemporaryDirectory() as d: + return run_tool_on_targets(ClippyDriver(build_data, d)) diff --git a/mesonbuild/scripts/run_tool.py b/mesonbuild/scripts/run_tool.py index bccc4cb833af..e206ff7fe8d7 100644 --- a/mesonbuild/scripts/run_tool.py +++ b/mesonbuild/scripts/run_tool.py @@ -6,6 +6,7 @@ import asyncio.subprocess import fnmatch import itertools +import json import signal import sys from pathlib import Path @@ -126,3 +127,12 @@ def run_clang_tool(name: str, srcdir: Path, builddir: Path, fn: T.Callable[..., def wrapper(path: Path) -> T.Iterable[T.Coroutine[None, None, int]]: yield fn(path, *args) return asyncio.run(_run_workers(all_clike_files(name, srcdir, builddir), wrapper)) + +def run_tool_on_targets(fn: T.Callable[[T.Dict[str, T.Any]], + T.Iterable[T.Coroutine[None, None, int]]]) -> int: + if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + with open('meson-info/intro-targets.json', encoding='utf-8') as fp: + targets = json.load(fp) + return asyncio.run(_run_workers(targets, fn))