Skip to content

Commit

Permalink
rust: new target rustdoc
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
bonzini committed Feb 2, 2025
1 parent c616f1e commit ae9cb54
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 0 deletions.
16 changes: 16 additions & 0 deletions mesonbuild/backend/ninjabackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
101 changes: 101 additions & 0 deletions mesonbuild/scripts/rustdoc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 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', '-l'}:
prev = arg
elif arg != '-g' and not arg.startswith('-l'):
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('-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))
18 changes: 18 additions & 0 deletions unittests/allplatformstests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit ae9cb54

Please sign in to comment.