This blog post is a zoom in into one subject of the Guide and Tips for crate publishing. It’s about managing your documentation and more specifically about using two unstable features of rustdoc:
- external_doc
- doc_cfg
external_doc
is a way to automatically include markdown files in your generated documentation. It’s main and obvious use case is so that you can show the README.md as the intro page of the API docs without copying the contents of the file in lib.rs
.
Update: external_doc is now removed and no longer needed. See this github issue for a workaround that will be needed until August 2021 when 1.54 is released.
doc_cfg
is a feature that allows you to specify that certain types are only available if certain features are turned on, or when compiling for a specific target. Unfortunately rustdoc doesn’t detect this automatically, you have to add an attribute to each conditionally available type.
The main challenge is using these unstable features in a crate that you want to compile on stable Rust. There are a few goals here:
- we want to enable these features for
cargo doc
. - we want to be able to run doctests for examples in the README, but want
cargo test
to function on stable. - we want
--all-features
to work fine on stable. - ideally, we fail
cargo doc
on stable, because it could never generate correct documentation.
For the purpose of this exploration I assume it’s acceptable that we have to run a special command for generating docs and doctests, as those operations are rare, and often can be scripted for CI and docs.rs.
Attempt 1: Using cargo features
My first attempt used a cargo feature. It’s a bit of a hack to use a cargo feature for this and it quickly becomes unwieldy as your project has more features, since --all-features
no longer works on stable.
Before rust 1.41 conditionally enabling these features didn’t break stable cargo doc
, but now, you really have to make sure to never run cargo doc
on stable.
Attempt 2: Using --cfg docs
@Yandros suggested adding this to Cargo.toml:
[package.metadata.docs.rs]
all-features = true
rustdocflags = ["--cfg", "docs"]
Add this to the .cargo/config file (create it if it doesn’t exist) to make it work locally:
[build]
rustdocflags = ["--cfg", "docs"]
Now we can enable the features based on a cfg attribute rather than on a cargo feature:
#![cfg_attr(docs,
feature(doc_cfg),
doc = include_str!("../README.md"),
)]
#![doc = ""] // empty doc line to handle missing doc warning when the feature is missing.
This is nice, we don’t bloat our cargo features and --all-features
just works again. However there are still some issues. cargo test
runs doc tests, and rustdocflags
will be enabled when running cargo test
, breaking it on stable now. I haven’t found a way to run cargo test
excluding doc tests, but on top of that, we probably want to keep most doc tests enabled on stable on a crate we want to be functional on stable. I also haven’t found more specific ways to detect cargo doc
, to only turn on the features with the doc
subcommand and when doing cargo test --doc
. Another dead end was looking for a way to manually specify --cfg docs
. I think it is possible to pass this as an extra rustc argument using environment variables or special flags, but that quickly becomes unwieldy for local development.
Attempt 3: rustc_version
What we really want is to say to the compiler: only enable this on nightly builds. This can be achieved only by adding an extra dependency and a build.rs. This is the best workaround I have found so far:
build.rs:
// Detect the rustc channel
//
use rustc_version::{ version_meta, Channel };
fn main()
{
// Set cfg flags depending on release channel
//
match version_meta().unwrap().channel
{
Channel::Stable => println!( "cargo:rustc-cfg=stable" ),
Channel::Beta => println!( "cargo:rustc-cfg=beta" ),
Channel::Nightly => println!( "cargo:rustc-cfg=nightly" ),
Channel::Dev => println!( "cargo:rustc-cfg=rustc_dev" ),
}
}
lib.rs:
#![ cfg_attr( nightly, feature(doc_cfg) ) ]
#![ cfg_attr( nightly, cfg_attr( nightly, doc = include_str!("../README.md") )) ]
#![ doc = "" ] // empty doc line to handle missing doc warning on stable.
The downsides are:
- requires an extra dependency and a build.rs.
cargo doc
will succeed on stable but not generate correct documentation. This is fine on docs.rs because they always run nightly, but be careful on CI builds.- slightly off topic: I have found no way to run doctests on WASM. There is
-Z doctest-xcompile
, but that has issues of it’s own. Namely it will require to add a.cargo/config
file to set up the test runner, but that will breakwasm-pack
on Travis and wasm-bindgen-cli bloats CI by having to compile from source and it doesn’t compile on windows. I haven’t gotten wasm-pack to work on windows either btw. - Yes, you need the double
cfg_attr
for now. Things should finally get sorted with rustdoc 1.54… Just a few more months.
Further reading
@Yandros has suggested some other tips for reducing boilerplate on using doc_cfg
. Be sure to check them out on the user forum.