a whirlwind tour of rustdoc

I’m generally a fan of rustdoc. I love that there’s effectively a one-touch access to creating good-looking documentation for my libraries. It’s not without its own issues, but the fact that it’s distributed alongside rust, and produces pretty good output, is enough to make me happy about it.

But before we get too far, what even is rustdoc? Put simply, it’s the thing that runs under-the-hood whenever you run cargo doc. It takes a crate as input, and generates a pile of HTML pages (and their supporting assets) as output. You can preview what the output looks like by checking out rust’s standard library documentation. By default, it includes the theme shown there, the search box that searches anything within that crate, and docs for all a crate’s dependencies, whose items are also included in that search index.

(For a bit of history: rustdoc is pretty old. The commit that added it dates to December 2011! That makes it almost as old as rustc itself (started in July 2009 and successfully self-hosted in April 2011), well before Rust 1.0 landed in May 2015.)

where oh where has my little crate gone

That’s all well and good, but what happens if you want to tweak something about it? Say, there’s a formatting issue you want to fix, or it’s not outputting docs for something you would expect it to pick up. Where is rustdoc?

It actually doesn’t have a repo of its own. It’s situated alongside the source for the rust compiler, and is built alongside it whenever you build the compiler as a whole. This means that if you want to tweak it and test out your change, you need to submit yourself to get used to the compiler’s build system. This can be an issue if you haven’t dealt with rustbuild before, or if you just want to test a small change.

show me the way to docsville

So here’s a bit of a crash course in rustbuild, the build system for the compiler. A better introduction is available in the main repo’s README, but here are the basics:

All interaction with the build system is either done through git (if you need to deal with submodules) or through a wrapper script called x.py. This provides a means to download known-good versions of rustc and cargo to compile the actual build system, written fully in rust. To get to just the parts we care about, there are two commands worth knowing: ./x.py build and ./x.py doc. The first just compiles the bits we ask for (or everything, if you don’t ask for something specific), and the second compiles everything and then renders the full suite of documentation: standard library docs, the book, the reference, anything you see at https://doc.rust-lang.org. It’s useful if your rustdoc change can be seen by looking at the standard library docs instead of having to render a separate crate’s docs for your test.

follow the rusty-brick road

If you just run one of those commands by themselves, you’ll start to notice something annoying: they compile a lot of things. This is the nature of building the compiler: it goes through a “bootstrap” sequence, where it starts with a known version (in this case, the most recent beta), builds the compiler, then uses the result to build the compiler again, just to prove that everything still works. (There’s an optional third step involving building a third time to prove its reproducible. This is turned off by default.)

If you were just working on the compiler or standard library, then you could get around this with the --stage and --keep-stage flags, but this doesn’t quite work when testing rustdoc on the standard library documentation. At least, as of this writing. So for now, if we want to test rustdoc the easy way, we’re stuck recompiling the whole stage to re-render the standard library docs. I’ll mention an alternate method later on.

To test changes to rustdoc by rendering the standard library documentation, make your changes to the code, then call ./x.py doc --stage 1 to build rustdoc and render the library documentation. This will compile just rustdoc on stage 0, then all of stage 1, then render the set of documentation. The rendering will be available in the build directory in your rust clone, specifically build/$TRIPLE/doc, where $TRIPLE is the “build triple” of the system you’re working on. In my case, it’s x86_64-unknown-linux-gnu, since I do my Rust development on a 64-bit Arch Linux system.

If you don’t want to wait for the entire stage to build every time, or if you need to test something that doesn’t have a good representative case in the standard library, then you can take your custom rustc and rustdoc and use them to render documentation for a separate crate. To do this, run ./x.py build src/libtest --stage 1 as your build step. This builds just enough that you get useable binaries for the next steps.

If you’re using rustup to manage your regular rust installs, it has a fantastic feature for people who work on the compiler. You can “link” a local directory to create a custom toolchain. To do this, run the command rustup toolchain link local /path/to/build/$TRIPLE/stage1. (Replace the path in the last argument with the actual path to the build directory in your Rust clone. The build/$TRIPLE/stage1 is the actual structure that needs to be at the end of the path, much like the standard library docs were in build/$TRIPLE/doc.)

Once you’ve set this up, you can use your “local” rustc and rustdoc just like you would use the “stable” or “nightly” toolchains! The “local” in that rustup command earlier set the name for a new toolchain. Therefore, you can generate documentation with cargo +local doc. If you’ve rebuilt your rustdoc but haven’t touched the other crate, you may need to run cargo clean beforehand so it actually rebuilds everything.

(If you don’t have rustup, don’t fret! It’s still possible to use your custom toolchain for a one-off documentation run. It’s just a little more complicated. Cargo recognizes two environment variables, RUSTC and RUSTDOC, which point to their respective binaries. By setting these two environment variables and calling cargo doc, you can make cargo use your custom toolchain even without rustup!)

the winding streets of the verdant rustdoc city

(caution: the only part of rustdoc i’ve extensively worked with is the part that generates the HTML; i make no claims of accuracy for any other component of rustdoc >_>)

Rustdoc uses the compiler-internal libraries, so it can read a crate the same way the compiler would. However, to guard itself against changes to the compiler, it takes the “raw” syntax tree and converts it into its own simplified version before working on it. This “doctree” version of the crate is then “cleaned” to strip the AST to only those elements that make sense for documentation (for example, items inside function bodies are never visible outside that function, so they’re removed when crawling the AST).

With this “clean” version in hand, it then generates the search index, writes out the static files that go with the output, and walks the crate, writing each item in turn to the appropriate folder and file. The formatting is actually implemented as a bunch of fmt::Display impls, so the template is literally a big write! call with the page template as the format string. This way, modules render links to all their items, traits render a listing of their methods and implementors, enums list their variants, and so on.

To render documentation comments, the Rust compiler first converts /// and //! comments into #[doc] attributes, then rustdoc collects all of these together and hands them off to a Markdown renderer to convert to HTML. The resulting HTML is then used wherever the documentation for an item needs to be.

At the moment, rustdoc defaults to using a renderer called Hoedown, but we’re currently testing an alternative implementation called pulldown-cmark that conforms to the CommonMark specification and allows for easier extension due to being written in Rust. As of Rust 1.18, you can test the pulldown version by passing --enable-commonmark to rustdoc, like with RUSTDOCFLAGS="--enable-commonmark" cargo doc.

If you’re asking rustdoc to run documentation tests, it runs most of the steps above, but stops short of emitting the final HTML. Instead, it collects all the documentation comments from the crate and hands them to the Markdown renderer to see what it picks up as a code block. From these code blocks, it reads through them to find whether it needs to remove a trailing # from hidden lines, or add the required extern crate line and fn main() wrapper, or even needs to process it at all due to a no_run or ignore flag on the block.

For those that need to run, it takes the code after processing and hands it off to the compiler to build the test. For those that need to run, it then executes the test and checks the result to evaluate the final test result.

pay no attention to those maintainers behind the curtain

Rustdoc is an official Rust project, and falls under the resposibility of the Dev tools team. In addition, there are three “rustdoc tool peers” who could be considered the primary maintainers of rustdoc: @steveklabnik, @GuillaumeGomez, and myself (@QuietMisdreavus). Issues against rustdoc can be posted on the main rust-lang/rust issue tracker, and short-form discussion about it can be had in the #rust-dev-tools IRC channel on the Mozilla IRC network.