You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Conception/drake-master/doc/doxygen_cxx/build.py

280 lines
9.9 KiB

"""Command-line tool to generate Drake's C++ API reference.
For instructions, see https://drake.mit.edu/documentation_instructions.html.
"""
import argparse
from fnmatch import fnmatch
import os
from os.path import join, relpath
import shutil
import sys
from bazel_tools.tools.python.runfiles import runfiles
from drake.doc.defs import (
check_call,
main,
perl_cleanup_html_output,
symlink_input,
verbose,
)
def _symlink_headers(*, drake_workspace, temp_dir, modules):
"""Prepare the input and output folders. We will copy the requested input
file(s) into a temporary scratch directory, so that Doxygen doesn't scan
the drake_workspace directly (which is extremely slow).
"""
# Locate the default top-level modules.
unwanted_top_level_dirs = [
".*", # There is no C++ code here.
"bazel-*", # Ignore Bazel build artifacts.
"build", # Ignore CMake build artifacts.
"cmake", # There is no C++ code here.
"debian", # Ignore Debian build artifacts.
"doc", # There is no C++ code here.
"gen", # Ignore setup artifacts.
"setup", # There is no C++ code here.
"third_party", # Only document first-party Drake code.
"tools", # There is no C++ code here.
"tutorials", # There is no C++ code here.
]
default_modules = [
f"drake.{x}" for x in os.listdir(drake_workspace)
if os.path.isdir(join(drake_workspace, x))
and not any([
fnmatch(x, unwanted)
for unwanted in unwanted_top_level_dirs
])
]
# Iterate modules one by one.
for module in (modules or default_modules):
if verbose():
print(f"Symlinking {module} ...")
prefix = "drake."
if not module.startswith(prefix):
print("error: Doxygen modules must start with 'drake',"
f" not {module}")
sys.exit(1)
module_as_subdir = module[len(prefix):].replace('.', '/')
module_workspace = join(drake_workspace, module_as_subdir)
if not os.path.isdir(module_workspace):
print(f"error: Unknown module {module}")
sys.exit(1)
for dirpath, dirs, files in os.walk(module_workspace):
subdir = relpath(dirpath, drake_workspace)
os.makedirs(join(temp_dir, "drake", subdir))
for item in files:
if any([module.startswith("drake.doc"),
"images" in subdir,
item.endswith(".h")]):
dest = join(temp_dir, "drake", subdir, item)
if not os.path.exists(dest):
os.symlink(join(dirpath, item), dest)
def _generate_doxyfile(*, manifest, out_dir, temp_dir, dot):
"""Creates Doxyfile_CXX from Doxyfile_CXX.in."""
input_filename = manifest.Rlocation(
"drake/doc/doxygen_cxx/Doxyfile_CXX.in")
assert os.path.exists(input_filename)
output_filename = join(temp_dir, "Doxyfile_CXX")
cmake_configure_file = manifest.Rlocation(
"drake/tools/workspace/cmake_configure_file")
assert os.path.exists(cmake_configure_file)
definitions = {}
definitions["INPUT_ROOT"] = temp_dir
definitions["OUTPUT_DIRECTORY"] = out_dir
if dot:
definitions["DOXYGEN_DOT_FOUND"] = "YES"
definitions["DOXYGEN_DOT_EXECUTABLE"] = dot
else:
definitions["DOXYGEN_DOT_FOUND"] = "NO"
definitions["DOXYGEN_DOT_EXECUTABLE"] = ""
check_call([
cmake_configure_file,
"--input", input_filename,
"--output", output_filename,
] + [
"-D%s=%s" % (key, value)
for key, value in definitions.items()
])
assert os.path.exists(output_filename)
return output_filename
def _generate_doxygen_header(*, doxygen, temp_dir):
"""Creates Drake's header.html based on a patch to Doxygen's default
header template.
"""
# This matches Doxyfile_CXX.
header_path = f"{temp_dir}/drake/doc/doxygen_cxx/header.html"
# Extract the default templates from the Doxygen binary. We only want the
# header, but it forces us to create all three in this exact order.
scratch_files = [
"header.html.orig",
"footer.html.orig",
"customdoxygen.css.orig",
]
check_call([doxygen, "-w", "html"] + scratch_files, cwd=temp_dir)
shutil.copy(f"{temp_dir}/header.html.orig", header_path)
for orig in scratch_files:
os.remove(f"{temp_dir}/{orig}")
# Apply our patch.
patch_file = f"{header_path}.patch"
check_call(["/usr/bin/patch", header_path, patch_file])
def _is_important_warning(line):
"""Returns true iff the given line of Doxygen output should be promoted to
a build error.
"""
# Check for broken links.
if "unable to resolve reference" in line:
# Maybe the code specifically wanted to have a broken link.
if "MODULE_NOT_WRITTEN_YET" in line:
return False
# For now, don't error on broken function signatures; only error on the
# more important links such as section cross-references. An example:
# ... unable to resolve ... `AssignRole(SourceId,...)' for ref ...
if "(" in line:
return False
# TODO(#14107) Remove this if-clause once the issue is resolved.
if "df_contact_material" in line:
return False
# Broken link.
return True
# All good.
return False
def _postprocess_doxygen_log(original_lines, check_for_errors):
"""If check_for_errors is true, then looks for any important warnings and
fails-fast if any were found. When in verbose mode, also dumps the log to
the console.
"""
# Throw away useless lines.
#
# Specifically, we remove stanzas that looks like this:
#
# The following parameters of DiscardZeroGradient(...) are not documented:
# parameter 'auto_diff_matrix'
#
# The pattern to remove will be the "are not documented" line, followed by
# some list of one or more parameter names.
lines = []
is_ignoring_parameters = False
for line in original_lines:
if is_ignoring_parameters:
if line.startswith("parameter "):
continue
is_ignoring_parameters = False
if line.endswith(" are not documented:"):
is_ignoring_parameters = True
continue
lines.append(line)
# Print all of the warnings (when requested).
if verbose():
for line in lines:
print("[doxygen] " + line)
# Check for important warnings (when requested).
errors = []
if check_for_errors:
for line in lines:
if _is_important_warning(line):
errors.append(line.replace("warning:", "error:"))
if errors:
message = "\n".join(["Problems found by Doxygen:"] + sorted(errors))
raise RuntimeError(message)
def _build(*, out_dir, temp_dir, modules, quick):
"""Generates into out_dir; writes scratch files into temp_dir.
As a precondition, both directories must already exist and be empty.
"""
manifest = runfiles.Create()
# Find drake's sources.
drake_workspace = os.path.dirname(os.path.realpath(
manifest.Rlocation("drake/.bazelproject")))
assert os.path.exists(drake_workspace), drake_workspace
assert os.path.exists(join(drake_workspace, "WORKSPACE")), drake_workspace
# Find doxygen.
doxygen = manifest.Rlocation("doxygen/doxygen")
assert os.path.exists(doxygen), doxygen
# Find dot.
dot = "/usr/bin/dot"
assert os.path.exists(dot), dot
# Configure doxygen.
doxyfile = _generate_doxyfile(
manifest=manifest,
out_dir=out_dir,
temp_dir=temp_dir,
dot=(dot if not quick else ""))
# Prepare our input.
symlink_input(
"drake/doc/doxygen_cxx/doxygen_input.txt", temp_dir)
_generate_doxygen_header(
doxygen=doxygen,
temp_dir=temp_dir,
)
_symlink_headers(
drake_workspace=drake_workspace,
temp_dir=temp_dir,
modules=modules)
# Run doxygen.
check_call([doxygen, doxyfile], cwd=temp_dir)
# Post-process its log, and check for errors. If we are building only a
# subset of the docs, we are likely to encounter errors due to the missing
# sections, so we'll only enable the promotion of warnings to errors when
# we're building all of the C++ documentation.
check_for_errors = (len(modules) == 0)
with open(f"{temp_dir}/doxygen.log", encoding="utf-8") as f:
lines = [
line.strip().replace(f"{temp_dir}/", "")
for line in f.readlines()
]
_postprocess_doxygen_log(lines, check_for_errors)
# Fix the formatting of deprecation text (see drake#15619 for an example).
extra_perl_statements = [
# Remove quotes around the removal date.
r's#(removed from Drake on or after) "(....-..-..)" *\.#\1 \2.#;',
# Remove all quotes within the explanation text, i.e., the initial and
# final quotes, as well as internal quotes that might be due to C++
# multi-line string literals.
# - The quotes must appear after a "_deprecatedNNNNNN" anchor.
# - The quotes must appear before a "<br />" end-of-line.
# Example lines:
# <dl class="deprecated"><dt><b><a class="el" href="deprecated.html#_deprecated000013">Deprecated:</a></b></dt><dd>"Use RotationMatrix::MakeFromOneVector()." <br /> # noqa
# <dd><a class="anchor" id="_deprecated000013"></a>"Use RotationMatrix::MakeFromOneVector()." <br /> # noqa
r'while (s#(?<=_deprecated\d{6}")([^"]*)"(.*?<br)#\1\2#) {};',
]
perl_cleanup_html_output(
out_dir=out_dir,
extra_perl_statements=extra_perl_statements)
# The nominal pages to offer for preview.
return ["", "classes.html", "modules.html"]
if __name__ == '__main__':
main(build=_build, subdir="doxygen_cxx", description=__doc__.strip(),
supports_modules=True, supports_quick=True)