Skip to content

Commit

Permalink
scripts: add "clippy" internal tool
Browse files Browse the repository at this point in the history
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 <pbonzini@redhat.com>
  • Loading branch information
bonzini authored and dcbaker committed Dec 19, 2024
1 parent 5dc537a commit 27c567d
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 0 deletions.
49 changes: 49 additions & 0 deletions mesonbuild/compilers/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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):

Expand Down
67 changes: 67 additions & 0 deletions mesonbuild/scripts/clippy.py
Original file line number Diff line number Diff line change
@@ -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))
10 changes: 10 additions & 0 deletions mesonbuild/scripts/run_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import asyncio.subprocess
import fnmatch
import itertools
import json
import signal
import sys
from pathlib import Path
Expand Down Expand Up @@ -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))

0 comments on commit 27c567d

Please sign in to comment.