Skip to main content

xtask/
docs.rs

1//! Build the mdBook guide and workspace rustdoc as one static site.
2
3use crate::util;
4use anyhow::{Context, Result, ensure};
5use std::{fs, path::Path, process::Command};
6
7pub fn build() -> Result<()> {
8    let root = util::root();
9    let staging = tempfile::tempdir()?;
10    let target = root.join("target/site-rustdoc");
11
12    // Keep the nested Cargo invocation separate from the running xtask's cache.
13    // Rustdoc output is cumulative, so remove old docs while retaining builds.
14    let status = util::command(&["cargo", "clean", "--doc", "--target-dir"])
15        .arg(&target)
16        .status()
17        .context("cleaning previous rustdoc output")?;
18
19    ensure!(status.success(), "rustdoc cleanup failed");
20
21    let status = util::command(&[
22        "cargo",
23        "doc",
24        "--locked",
25        "--workspace",
26        "--no-deps",
27        "--document-private-items",
28        "--target-dir",
29    ])
30    .arg(&target)
31    .env("CARGO_ENCODED_RUSTDOCFLAGS", "-Dwarnings")
32    .status()
33    .context("building workspace rustdoc")?;
34
35    ensure!(status.success(), "rustdoc build failed");
36
37    stage(&root, staging.path())?;
38
39    let status = util::command(&["mdbook", "build"])
40        .arg(staging.path())
41        .arg("--dest-dir")
42        .arg(root.join("_site"))
43        .status()
44        .context("running mdbook; install it with mise install github:rust-lang/mdBook")?;
45
46    ensure!(status.success(), "mdBook build failed");
47    publish_api(&target.join("doc"), &root.join("_site"))?;
48
49    Ok(())
50}
51
52/// Publish the whole rustdoc tree, including shared search and source assets.
53pub fn publish_api(source: &Path, site: &Path) -> Result<()> {
54    let destination = site.join("api");
55
56    // Fail before replacing the previous API if generation produced no docs.
57    ensure!(
58        source.join("crates.js").is_file(),
59        "missing rustdoc crate index"
60    );
61
62    if destination.exists() {
63        fs::remove_dir_all(&destination)?;
64    }
65
66    for path in util::files(source)? {
67        let target = destination.join(path.strip_prefix(source)?);
68
69        fs::create_dir_all(target.parent().context("rustdoc file parent")?)?;
70        fs::copy(path, target)?;
71    }
72
73    fs::write(
74        destination.join("index.html"),
75        "<!doctype html><html lang=\"en\"><meta charset=\"utf-8\">\
76         <title>Rust API</title>\
77         <meta http-equiv=\"refresh\" content=\"0; url=../docs/rust-api.html\">\
78         <a href=\"../docs/rust-api.html\">Rust API reference</a></html>\n",
79    )?;
80
81    Ok(())
82}
83
84pub fn stage(root: &Path, destination: &Path) -> Result<()> {
85    let tracked = Command::new("git")
86        .args(["ls-files", "-z", "--", ":(attr:site)"])
87        .current_dir(root)
88        .output()?;
89    let paths = util::checked(tracked)?;
90
91    fs::create_dir_all(destination)?;
92    fs::copy(root.join("book.toml"), destination.join("book.toml"))?;
93
94    // Use working-tree contents, but only tracked files: local benchmark runs
95    // and model downloads must never become website assets.
96    for name in paths.split('\0').filter(|name| !name.is_empty()) {
97        let path = Path::new(name);
98        let target = destination.join(path);
99
100        fs::create_dir_all(target.parent().context("site file parent")?)?;
101        fs::copy(root.join(path), &target)?;
102    }
103
104    Ok(())
105}