From 6471bdae42918652e9adcd2cd1022c9194c17eb6 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 20 Dec 2024 18:00:58 +0100 Subject: [PATCH] rust: new target rustdoc Another rust tool, another copy of roughly the same code as clippy and rustfmt. Apart from the slightly different command lines, the output is in a directory and test targets are skipped. Knowing the output directory can be useful, so print that on successful execution of rustdoc. Signed-off-by: Paolo Bonzini --- mesonbuild/backend/ninjabackend.py | 16 +++++ mesonbuild/scripts/rustdoc.py | 103 +++++++++++++++++++++++++++++ unittests/allplatformstests.py | 18 +++++ 3 files changed, 137 insertions(+) create mode 100644 mesonbuild/scripts/rustdoc.py diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 58d2e8fae2e4..377d62af8702 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -3727,6 +3727,21 @@ def generate_clippy(self) -> None: elem.add_dep(list(self.all_structured_sources)) self.add_build(elem) + def generate_rustdoc(self) -> None: + if 'rustdoc' in self.all_outputs or not self.have_language('rust'): + return + + cmd = self.environment.get_build_command() + \ + ['--internal', 'rustdoc', self.environment.build_dir] + elem = self.create_phony_target('rustdoc', 'CUSTOM_COMMAND', 'PHONY') + elem.add_item('COMMAND', cmd) + elem.add_item('pool', 'console') + for crate in self.rust_crates.values(): + if crate.crate_type in {'rlib', 'dylib', 'proc-macro'}: + elem.add_dep(crate.target_name) + elem.add_dep(list(self.all_structured_sources)) + self.add_build(elem) + def generate_scanbuild(self) -> None: if not environment.detect_scanbuild(): return @@ -3795,6 +3810,7 @@ def generate_utils(self) -> None: self.generate_clangformat() self.generate_clangtidy() self.generate_clippy() + self.generate_rustdoc() self.generate_tags('etags', 'TAGS') self.generate_tags('ctags', 'ctags') self.generate_tags('cscope', 'cscope') diff --git a/mesonbuild/scripts/rustdoc.py b/mesonbuild/scripts/rustdoc.py new file mode 100644 index 000000000000..4ffe33f98048 --- /dev/null +++ b/mesonbuild/scripts/rustdoc.py @@ -0,0 +1,103 @@ +# 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 +from ..wrap import WrapMode, wrap + +if T.TYPE_CHECKING: + from ..compilers.rust import RustCompiler + +async def run_and_confirm_success(cmdlist: T.List[str], crate: str) -> None: + returncode = await run_with_buffered_output(cmdlist) + if returncode == 0: + print(mlog.green('Generated'), os.path.join('doc', crate)) + return returncode + +class Rustdoc: + def __init__(self, build: build.Build, tempdir: str, subprojects: T.Set[str]) -> None: + self.tools: PerMachine[T.List[str]] = PerMachine([], []) + self.warned: T.DefaultDict[str, bool] = defaultdict(lambda: False) + self.tempdir = tempdir + self.subprojects = subprojects + 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('rustdoc', build.environment) + + def warn_missing_rustdoc(self, machine: str) -> None: + if self.warned[machine]: + return + mlog.warning(f'rustdoc 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]]: + if target['subproject'] is not None and target['subproject'] not in self.subprojects: + return + + for src_block in target['target_sources']: + if 'compiler' in src_block and src_block['language'] == 'rust': + rustdoc = getattr(self.tools, src_block['machine']) + if not rustdoc: + self.warn_missing_rustdoc(src_block['machine']) + continue + + cmdlist = list(rustdoc) + prev = None + crate_name = None + is_test = False + for arg in src_block['parameters']: + if prev: + if prev == '--crate-name': + cmdlist.extend((prev, arg)) + crate_name = arg + prev = None + continue + + if arg == '--test': + is_test = True + break + elif arg in {'--crate-name', '--emit', '--out-dir'}: + prev = arg + elif arg != '-g': + cmdlist.append(arg) + + if is_test: + # --test has a completely different meaning for rustc and rustdoc; + # when using rust.test(), only the non-test target is documented + continue + if crate_name: + cmdlist.extend(src_block['sources']) + # Assume documentation is generated for the developer's use + cmdlist.append('--document-private-items') + cmdlist.append('--check-cfg') + cmdlist.append('cfg(docsrs)') + cmdlist.append('-o') + cmdlist.append('doc') + yield run_and_confirm_success(cmdlist, crate_name) + else: + print(mlog.yellow('Skipping'), target['name'], '(no crate name)') + +def get_nonwrap_subprojects(build_data: build.Build) -> T.Set[str]: + wrap_resolver = wrap.Resolver( + build_data.environment.get_source_dir(), + build_data.subproject_dir, + wrap_mode=WrapMode.nodownload) + return set(sp + for sp in build_data.environment.coredata.initialized_subprojects + if sp and (sp not in wrap_resolver.wraps or wrap_resolver.wraps[sp].type is None)) + +def run(args: T.List[str]) -> int: + os.chdir(args[0]) + build_data = build.load(os.getcwd()) + subproject_list = get_nonwrap_subprojects(build_data) + with tempfile.TemporaryDirectory() as d: + return run_tool_on_targets(Rustdoc(build_data, d, subproject_list)) diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index 7c2d3ba61ac3..97ada92e5015 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -4884,6 +4884,24 @@ 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('rustdoc'), 'Test requires rustdoc') + def test_rustdoc(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 = os.path.join(tmpdir, 'a') + shutil.copytree(os.path.join(self.rust_test_dir, '9 unit tests'), + testdir) + self.init(testdir) + self.build('rustdoc') + 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: