Skip to content

Commit

Permalink
doc: Add direct styleguide publication (RobotLocomotion#14811)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwnimmer-tri authored Mar 26, 2021
1 parent c8be496 commit 608bab0
Show file tree
Hide file tree
Showing 11 changed files with 411 additions and 5 deletions.
12 changes: 12 additions & 0 deletions doc/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ load(
)
load("//tools/lint:lint.bzl", "add_lint_tests")

drake_py_library(
name = "defs",
srcs = ["defs.py"],
visibility = ["//doc:__subpackages__"],
deps = [
"@bazel_tools//tools/python/runfiles",
],
)

# For maximum browser compatibility these should be at the root of the
# generated website and should not be renamed.
filegroup(
Expand Down Expand Up @@ -123,6 +132,7 @@ drake_py_binary(
":gen_jekyll",
"//doc/doxygen_cxx:build",
"//doc/pydrake:gen_sphinx",
"//doc/styleguide:build",
],
tags = DEFAULT_BINARY_TAGS,
deps = [
Expand All @@ -141,6 +151,7 @@ filegroup(
"//doc/doxygen_cxx:build",
"//doc/pydrake:gen_sphinx",
"//doc/pydrake:serve_sphinx",
"//doc/styleguide:build",
],
tags = ["manual"],
)
Expand All @@ -153,6 +164,7 @@ test_suite(
tests = [
":gen_jekyll_test",
"//doc/pydrake:gen_sphinx_test",
"//doc/styleguide:build_test",
],
)

Expand Down
1 change: 1 addition & 0 deletions doc/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ doc/
├── _pages/ - Jekyll collection: Add'l root-level pages. Can render Markdown.
├── pydrake/ - Python API reference.
├── _release-notes/ - Jekyll collection: Index of versioned releases.
├── styleguide/ - Drake Style Guide external.
├── third_party/ - Third party assets.
└── index.md - Home page.
```
Expand Down
12 changes: 12 additions & 0 deletions doc/_pages/documentation_instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ To generate the Python API documentation:
$ bazel run //doc/pydrake:serve_sphinx [-- --browser=false]
```

# Style Guide

To locally preview the Drake Style Guide:

```
$ bazel run //doc/styleguide:build -- --serve
```

To preview a local branch of the styleguide, set the
[local_repository_override](https://github.com/RobotLocomotion/drake/blob/master/tools/workspace/README.md#exploring-github_archive-changes-from-a-local-clone)
option in ``drake/tools/workspace/styleguide/`` before running the preview.

# Continuous Integration

To check that the documentation will pass Drake's Jenkins CI builds:
Expand Down
6 changes: 3 additions & 3 deletions doc/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,19 @@ def main():
gen_sphinx = manifest.Rlocation("drake/doc/pydrake/gen_sphinx")
gen_jekyll = manifest.Rlocation("drake/doc/gen_jekyll")
doxygen = manifest.Rlocation("drake/doc/doxygen_cxx/build")
for item in [gen_sphinx, gen_jekyll, doxygen]:
styleguide_build = manifest.Rlocation("drake/doc/styleguide/build")
for item in [gen_sphinx, gen_jekyll, doxygen, styleguide_build]:
assert os.path.exists(item), item

_check_call([gen_jekyll, f"--out_dir={out_dir}"])
_check_call([gen_sphinx, f"--out_dir={out_dir}/pydrake"])
_check_call([styleguide_build, f"--out_dir={out_dir}/styleguide"])
doxygen_scratch = f"{out_dir}/doxygen_scratch"
_check_call([doxygen, f"--out_dir={doxygen_scratch}"])
print(f"+ mv {doxygen_scratch}/html {out_dir}/doxygen_cxx")
os.rename(f"{doxygen_scratch}/html", f"{out_dir}/doxygen_cxx")
print(f"+ rm -rf {doxygen_scratch}")
shutil.rmtree(doxygen_scratch)
# TODO(jwnimmer-tri) Incorporate the Drake styleguide publication here,
# instead of having it be a separate pipeline.

_build_sitemap(out_dir)

Expand Down
40 changes: 38 additions & 2 deletions doc/defs.bzl
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# -*- python -*-

# This file contains constants that help define Drake's documentation targets
# (i.e., these should only be used for BUILD files within @drake//doc/...).
# This file contains build macros and constants that help define Drake's
# documentation targets (i.e., these should only be used for BUILD files
# within @drake//doc/...).

# Unless `setup/ubuntu/install_prereqs.sh --with-doc-only` has been run, most
# targets in //doc/... will fail to build, so by default we'll disable them.
Expand All @@ -21,3 +22,38 @@ DEFAULT_TEST_TAGS = [
# ecosystems might be doing so without us being aware.
"block-network",
]

def _enumerate_filegroup_impl(ctx):
out = ctx.actions.declare_file(ctx.attr.name)
runpaths = {}
for x in ctx.attr.data:
for y in x.data_runfiles.files.to_list():
if y.short_path.startswith("../"):
runpath = y.short_path[3:]
else:
runpath = ctx.workspace_name + "/" + y.short_path
runpaths[runpath] = True
result = sorted(runpaths.keys())
ctx.actions.write(out, "\n".join(result) + "\n")

# Return the new file to our caller.
return [DefaultInfo(
files = depset([out]),
data_runfiles = ctx.runfiles(files = [out]),
)]

enumerate_filegroup = rule(
implementation = _enumerate_filegroup_impl,
doc = """
Creates a text file listing the files incorporated into the given filegroup.
The listing is transitive (includes both filegroup.srcs and filegroup.data).
https://docs.bazel.build/versions/master/be/general.html#filegroup
""",
attrs = {
"data": attr.label_list(
allow_empty = False,
doc = "Filegroup whose data we should enumerate",
),
},
)
197 changes: 197 additions & 0 deletions doc/defs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""Common library to provide a reusable main() routine for all of our
documentation generation tools.
"""

import argparse
import functools
from http.server import SimpleHTTPRequestHandler
import os.path
from os.path import join
from socketserver import TCPServer
import shlex
import subprocess
from subprocess import PIPE, STDOUT
import tempfile

from bazel_tools.tools.python.runfiles import runfiles

# This global variable can be toggled by our main() function.
_verbose = False


def verbose():
"""Returns True iff doc builds should produce detailed console output."""
return _verbose


def symlink_input(filegroup_resource_path, temp_dir, strip_prefix=None):
"""Symlinks a rule's input data into a temporary directory.
This is useful both to create a hermetic set of inputs to pass to a
documentation builder, or also in case we need to adjust the input data
before passing it along.
Args:
filegroup_resource_path: Names a file created by enumerate_filegroup
(in defs.bzl) which contains resource paths.
temp_dir: Destination directory, which must already exist.
strip_prefix: Optional; a list[str] of candidate strings to remove
from the resource path when linking into temp_dir. The first match
wins, and it is valid for no prefixes to match.
"""
assert os.path.isdir(temp_dir)
r = runfiles.Create()
with open(r.Rlocation(filegroup_resource_path)) as f:
input_filenames = f.read().splitlines()
for name in input_filenames:
orig_name = r.Rlocation(name)
assert os.path.exists(orig_name), name
dest_name = name
for prefix in (strip_prefix or []):
if dest_name.startswith(prefix):
dest_name = dest_name[len(prefix):]
break
temp_name = join(temp_dir, dest_name)
os.makedirs(os.path.dirname(temp_name), exist_ok=True)
os.symlink(orig_name, temp_name)


def check_call(args, *, cwd=None):
"""Runs a subprocess command, raising an exception iff the process fails.
Obeys the command-line verbosity flag for console output:
- when in non-verbose mode, shows output only in case of an error;
- when in verbose mode, shows the command-line and live output.
Args:
args: Passed to subprocess.run(args=...).
"""
echo = "+ " + " ".join([shlex.quote(x) for x in args])
if verbose():
print(echo, flush=True)
proc = subprocess.run(args, cwd=cwd, stderr=STDOUT)
else:
proc = subprocess.run(args, cwd=cwd, stderr=STDOUT, stdout=PIPE,
encoding='utf-8')
if proc.returncode != 0:
print(echo, flush=True)
print(proc.stdout, end='', flush=True)
proc.check_returncode()


def _call_build(*, build, out_dir):
"""Calls build() into out_dir, while also supplying a temp_dir."""
with tempfile.TemporaryDirectory(
dir=os.environ.get("TEST_TMPDIR"),
prefix="doc_builder_temp_") as temp_dir:
return build(out_dir=out_dir, temp_dir=temp_dir)


class _HttpHandler(SimpleHTTPRequestHandler):
"""An HTTP handler without logging."""

def log_request(*_):
pass


def _do_preview(*, build, subdir, port):
"""Implements the "serve" (http) mode of main().
Args:
build: Same as per main().
subdir: Same as per main().
port: Local port number to serve on, per the command line.
"""
print("Generating documentation preview ...")
with tempfile.TemporaryDirectory(prefix="doc_builder_preview_") as scratch:
if subdir:
out_dir = join(scratch, subdir)
os.mkdir(out_dir)
else:
out_dir = scratch
pages = _call_build(build=build, out_dir=out_dir)
assert len(pages) > 0
os.chdir(scratch)
print(f"The files have temporarily been generated into {scratch}")
print()
print("Serving at the following URLs for local preview:")
print()
for page in pages:
print(f" http://127.0.0.1:{port}/{join(subdir, page)}")
print()
print("Use Ctrl-C to exit.")
TCPServer.allow_reuse_address = True
server = TCPServer(("127.0.0.1", port), _HttpHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
print()
return


def _do_generate(*, build, out_dir, on_error):
"""Implements the "generate" (file output) mode of main().
Args:
build: Same as per main().
out_dir: Directory to generate into, per the command line.
on_error: Callback function to report problems with out_dir.
"""
if out_dir == "<test>":
out_dir = join(os.environ["TEST_TMPDIR"], "_builder_out")
if not os.path.isabs(out_dir):
on_error(f"--out_dir={out_dir} is not an absolute path")
if os.path.exists(out_dir):
if len(os.listdir(out_dir)) > 0:
on_error(f"--out_dir={out_dir} is not empty")
else:
if verbose():
print(f"+ mkdir -p {out_dir}", flush=True)
os.makedirs(out_dir)
print("Generating HTML ...")
pages = _call_build(build=build, out_dir=out_dir)
assert len(pages) > 0
print("... done")


def main(*, build, subdir, description):
"""Reusable main() function for documentation binaries; processes
command-line arguments and generates documentation.
Args:
build: Callback function to compile the documentation.
subdir: A subdirectory to use when offering preview mode on a local web
server; this does NOT affect the --out_dir path.
description: Main help str for argparse; typically the caller's __doc__.
"""
parser = argparse.ArgumentParser(description=description)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--serve", action='store_true',
help="Serve the documentation on the given PORT for easy preview.")
group.add_argument(
"--out_dir", type=str, metavar="DIR",
help="Generate the documentation to the given output directory."
" The DIR must be an absolute path."
" If DIR already exists, then it must be empty."
" (For regression testing, the DIR can be the magic value <test>,"
" in which case a $TEST_TMPDIR subdir will be used.)")
parser.add_argument(
"--port", type=int, metavar="PORT", default=8000,
help="Use a non-default PORT when serving for preview.")
parser.add_argument(
"--verbose", action="store_true",
help="Echo detailed commands, progress, etc. to the console")
args = parser.parse_args()
if args.verbose:
global _verbose
_verbose = True
if args.out_dir is None:
assert args.serve
_do_preview(build=build, subdir=subdir, port=args.port)
else:
_do_generate(build=build, out_dir=args.out_dir,
on_error=parser.error)


if __name__ == '__main__':
main()
54 changes: 54 additions & 0 deletions doc/styleguide/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- python -*-

package(default_visibility = ["//visibility:private"])

load(
"@drake//tools/skylark:drake_py.bzl",
"drake_py_binary",
)
load(
"//doc:defs.bzl",
"DEFAULT_BINARY_TAGS",
"DEFAULT_TEST_TAGS",
"enumerate_filegroup",
)
load("//tools/lint:lint.bzl", "add_lint_tests")

filegroup(
name = "jekyll_input",
srcs = [
":_config.yml",
":_layouts/default.html",
"@styleguide//:cppguide.html",
"@styleguide//:include/link.png",
"@styleguide//:include/styleguide.css",
"@styleguide//:include/styleguide.js",
"@styleguide//:pyguide.md",
],
)

enumerate_filegroup(
name = "jekyll_input.txt",
data = [":jekyll_input"],
)

drake_py_binary(
name = "build",
srcs = ["build.py"],
add_test_rule = 1,
data = [
":jekyll_input",
":jekyll_input.txt",
],
tags = DEFAULT_BINARY_TAGS,
test_rule_args = ["--out_dir=<test>"],
test_rule_size = "medium",
test_rule_tags = DEFAULT_TEST_TAGS,
test_rule_timeout = "short",
visibility = ["//doc:__pkg__"],
deps = [
"//doc:defs",
],
)

add_lint_tests()
Loading

0 comments on commit 608bab0

Please sign in to comment.