From 184c7891a4061813a51e026d7b737fe9ea0bb869 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 20 Nov 2024 02:01:13 +0100 Subject: [PATCH] rust: new targets rustfmt and rustfmt-check 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 --- docs/markdown/snippets/rustfmt.md | 6 +++ mesonbuild/backend/ninjabackend.py | 15 ++++++++ mesonbuild/compilers/rust.py | 4 +- mesonbuild/scripts/rustfmt.py | 59 ++++++++++++++++++++++++++++++ unittests/allplatformstests.py | 20 ++++++++++ 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 docs/markdown/snippets/rustfmt.md create mode 100644 mesonbuild/scripts/rustfmt.py diff --git a/docs/markdown/snippets/rustfmt.md b/docs/markdown/snippets/rustfmt.md new file mode 100644 index 000000000000..a7ef5389101a --- /dev/null +++ b/docs/markdown/snippets/rustfmt.md @@ -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. diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 0c34e08deb39..4c71da9a78ed 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -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 @@ -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') diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py index 717d5635f842..5de121d6a1f8 100644 --- a/mesonbuild/compilers/rust.py +++ b/mesonbuild/compilers/rust.py @@ -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 @@ -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): diff --git a/mesonbuild/scripts/rustfmt.py b/mesonbuild/scripts/rustfmt.py new file mode 100644 index 000000000000..53558ff306fb --- /dev/null +++ b/mesonbuild/scripts/rustfmt.py @@ -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 + +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 [])) diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index 6544bcce19ff..e1250e5c8bff 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -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: