Skip to content

Commit

Permalink
rust: new targets rustfmt and rustfmt-check
Browse files Browse the repository at this point in the history
This is very similar to clippy, with different command line of course.
Also it can change files, so do not run it twice on the same file.

Signed-off-by: Paolo Bonzini <[email protected]>
  • Loading branch information
bonzini committed Dec 19, 2024
1 parent 3f5469c commit 184c789
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 2 deletions.
6 changes: 6 additions & 0 deletions docs/markdown/snippets/rustfmt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Meson can run "rustfmt" on Rust projects

Meson now defines `rustfmt` and `rustfmt-check` targets if the project
uses the Rust programming language. The target runs rustfmt on all Rust
sources, using the `rustfmt` program from the same Rust toolchain as the
`rustc` compiler.
15 changes: 15 additions & 0 deletions mesonbuild/backend/ninjabackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3659,6 +3659,20 @@ def generate_clippy(self) -> None:
elem.add_dep(crate.target_name)
self.add_build(elem)

def generate_rustfmt(self) -> None:
if not self.have_language('rust'):
return

for target, args in {'rustfmt': [], 'rustfmt-check': ['--check']}.items():
if target in self.all_outputs:
continue
cmd = self.environment.get_build_command() + \
['--internal', 'rustfmt'] + args + [self.environment.build_dir]
elem = self.create_phony_target(target, 'CUSTOM_COMMAND', 'PHONY')
elem.add_item('COMMAND', cmd)
elem.add_item('pool', 'console')
self.add_build(elem)

def generate_scanbuild(self) -> None:
if not environment.detect_scanbuild():
return
Expand Down Expand Up @@ -3727,6 +3741,7 @@ def generate_utils(self) -> None:
self.generate_clangformat()
self.generate_clangtidy()
self.generate_clippy()
self.generate_rustfmt()
self.generate_tags('etags', 'TAGS')
self.generate_tags('ctags', 'ctags')
self.generate_tags('cscope', 'cscope')
Expand Down
4 changes: 2 additions & 2 deletions mesonbuild/compilers/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ 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]:
def get_rust_tool(self, name: str, env: Environment, keep_args: bool = True) -> 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
Expand All @@ -299,7 +299,7 @@ def get_rust_tool(self, name: str, env: Environment) -> T.List[str]:
else:
return []

return exelist + args
return exelist + args if keep_args else exelist


class ClippyRustCompiler(RustCompiler):
Expand Down
59 changes: 59 additions & 0 deletions mesonbuild/scripts/rustfmt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2024 The Meson development team

from __future__ import annotations
import argparse
import os
import sys
import typing as T

from .run_tool import run_tool_on_targets, run_with_buffered_output
from .. import build, mlog
from ..mesonlib import MachineChoice

if T.TYPE_CHECKING:
from ..compilers.rust import RustCompiler

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'RustCompiler' is not used.

class Rustfmt:
def __init__(self, rustfmt: T.List[str], args: T.List[str]):
self.args = rustfmt + args
self.done: T.Set[str] = set()

def __call__(self, target: T.Dict[str, T.Any]) -> T.Iterable[T.Coroutine[T.Any, T.Any, int]]:
for src_block in target['target_sources']:
if src_block['language'] == 'rust':
file = src_block['sources'][0]
if file in self.done:
continue
self.done.add(file)

cmdlist = list(self.args)
for arg in src_block['parameters']:
if arg.startswith('--color=') or arg.startswith('--edition='):
cmdlist.append(arg)

cmdlist.append(src_block['sources'][0])
yield run_with_buffered_output(cmdlist)

def run(args: T.List[str]) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--check', action='store_true')
parser.add_argument('builddir')
options = parser.parse_args(args)

os.chdir(options.builddir)
build_data = build.load(os.getcwd())

rustfmt: T.Optional[T.List[str]] = None
for machine in MachineChoice:
compilers = build_data.environment.coredata.compilers[machine]
if 'rust' in compilers:
compiler = T.cast('RustCompiler', compilers['rust'])
rustfmt = compiler.get_rust_tool('rustfmt', build_data.environment, False)
if rustfmt:
break
else:
mlog.error('rustfmt not found')
sys.exit(1)

return run_tool_on_targets(Rustfmt(rustfmt, ['--check'] if options.check else []))
20 changes: 20 additions & 0 deletions unittests/allplatformstests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4884,6 +4884,26 @@ def output_name(name, type_):
with self.subTest(key='{}.{}'.format(data_type, file)):
self.assertEqual(res[data_type][file], details)

@skip_if_not_language('rust')
@unittest.skipIf(not shutil.which('rustfmt'), 'Test requires rustfmt')
def test_rustfmt(self) -> None:
if self.backend is not Backend.ninja:
raise unittest.SkipTest('Rust is only supported with ninja currently')
try:
with tempfile.TemporaryDirectory() as tmpdir:
testdir = self.copy_srcdir(os.path.join(self.rust_test_dir, '9 unit tests'))
self.init(testdir)
with self.assertRaises(subprocess.CalledProcessError) as cm:
self.build('rustfmt-check')

self.build('rustfmt')
self.build('rustfmt-check')
except PermissionError:
# When run under Windows CI, something (virus scanner?)
# holds on to the git files so cleaning up the dir
# fails sometimes.
pass

@skip_if_not_language('rust')
@unittest.skipIf(not shutil.which('clippy-driver'), 'Test requires clippy-driver')
def test_rust_clippy(self) -> None:
Expand Down

0 comments on commit 184c789

Please sign in to comment.