1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! Build script helper which copies media and assets from a source directory below your
//! current crate to the target output directory of rustdoc.
//!
//! rustdoc currently does not support copying media files to the documentation output
//! directory. Pictures can only be included if they can be referenced as an online resource.
//!
//! This crate mitigates the problem. Add a call to
//! [`copy_assets_folder()`](fn.copy_assets_folder.html) to the build script `build.rs` of
//! your crate. This will copy the specified source directory to the rustdoc output directory.
//!
//! Source: [https://mrh.io/cargo-build-images-in-rust-docs/](https://mrh.io/cargo-build-images-in-rust-docs/)
//!
//! # Example
//!
//! Consider the following directory structure of crate "foo":
//! ```text
//! .
//! ├── build.rs
//! ├── Cargo.toml
//! ├── changelog.md
//! ├── doc
//! │   └── img
//! │       └── it-works.svg
//! ├── readme.md
//! ├── src
//! │   └── lib.rs
//! └── target
//! ```
//!
//! In this example, a call to `cargo doc` would create the API documentation in
//! `./target/doc/foo`. We want to include the file `doc/img/it-works.svg` in the crate
//! documentation directory.
//!
//! To do this, add a build dependency to `rustdoc-assets` in your `Cargo.toml`:
//!
//! ```toml
//! [build-dependencies]
//! rustdoc-assets = "0.2"
//! ```
//!
//! In `build.rs` do:
//! ```
//! # // simulate cargo environment variables so that the test compiles
//! # std::env::set_var("HOST", "x86_64-unknown-linux-gnu");
//! # std::env::set_var("TARGET", "x86_64-unknown-linux-gnu");
//! rustdoc_assets::copy_assets_folder("doc/img");
//! ```
//!
//! This will copy `./doc/img` to `./target/doc/foo/img`. In the rustdoc comment the
//! images can then be referenced through an HTML-tag as follows:
//!
//! ```html
//! /// <div align="center">
//! /// <img src="img/it-works.svg" width="200" />
//! /// </div>
//! ```
//!
//! <div align="center">
//! <img src="img/it-works.svg" width="200" /><br/>
//! <span style="font-size: small">Source: <a href="https://en.m.wikipedia.org/wiki/File:SMirC-thumbsup.svg">Wikipedia (CC)</a></span>
//! </div>
//!
//! # Update 2021-10-16
//!
//! In Rust 1.55 cargo doc now auto-cleans the `target/doc`-directory before generating
//! the documentation. However, rustdoc-assets uses the build script and only executes
//! during calls to cargo build/check. If cargo doc is executed afterwards the folders
//! subsequently get removed. I currently do not have a better solution than to at least
//! run cargo check one more time after cargo doc.
//!

use std::convert::AsRef;
use std::path::{Path, PathBuf};
use std::{env, fs};

const COPY_OPTS: fs_extra::dir::CopyOptions = fs_extra::dir::CopyOptions {
    overwrite: true,
    skip_exist: false,
    buffer_size: 64000,
    copy_inside: false,
    content_only: false,
    depth: 0,
};

/// Copy media files (images, etc) to your rustdoc output directory.
///
/// `source` is the path to your assets base-folder(i. e. "doc/images").
/// The path is relative to the location of your <span style="text-decoration:
/// underline">crate</span>'s manifest file Cargo.toml.
///
/// The default output directory for documentation is `target/doc/`.
///
/// rustdoc supports two ways of changing this output directory:
/// The command line flag `--target-dir` and the environment variable `CARGO_TARGET_DIR`.
/// `rustdoc-assets` cannot know about the runtime command line argument, but it does
/// check the environment variable for changes to the default output directory.
///
/// If your package is part of a workspace, create the following file as part of your
/// workspace `.cargo/config.toml`:
///
/// ```toml
/// [env]
/// ## points to the current working directory; required for rustdoc-assets
/// CARGO_WORKSPACE_DIR = { value = "", relative = true }
/// ```
///
/// This will cause `copy_assets_folder()` to use the workspace directory as base path,
/// instead of your crate root directory.
///
/// See the [Crate documentation](index.html#example) for an example.
///
pub fn copy_assets_folder<T>(source: T)
where
    T: AsRef<Path>,
{
    // Should panic if these aren't a thing
    let host = env::var("HOST").unwrap();
    let crate_name = std::env::var("CARGO_PKG_NAME").unwrap();
    let target = std::env::var("TARGET").unwrap();
    let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| String::from("target"));
    let workspace_dir = std::env::var("CARGO_WORKSPACE_DIR").ok();
    println!("host: {}", host);
    println!("target: {}", target);
    println!("target_dir: {:?}", target_dir);
    println!("workspace_dir: {:?}", workspace_dir);

    // If the target is the same as the host AND no target is
    // _explicitly specified, the directory will be ./target/doc,
    // Elsewhere, the directory will be ./target/{triple}/doc.
    // FIXME: target == host WITH explicitly specified --target
    //
    // Also, Gotta deal with crates that might have a dash in their name
    let sanitized_name = crate_name.replace('-', "_");
    let mut docs_dir = if let Some(ws) = workspace_dir {
        vec![ws]
    } else {
        Vec::new()
    };
    if host != target {
        docs_dir.extend_from_slice(&[target_dir, target, String::from("doc"), sanitized_name]);
    } else {
        docs_dir.extend_from_slice(&[target_dir, String::from("doc"), sanitized_name])
    }
    let docs_dir = docs_dir.join("/");
    let docs_dir_path = Path::new(&docs_dir);

    // Pre-emptively create the directory and copy ./doc/img into there
    fs::create_dir_all(&docs_dir).unwrap();
    fs_extra::copy_items(&[&source], &docs_dir, &COPY_OPTS).unwrap();

    let mut change_source = PathBuf::new();
    change_source.push(&source);
    // source directory changed
    println!(
        "cargo:rerun-if-changed={}",
        change_source.as_path().display()
    );
    // destination directory changed (i. e. if cargo-doc was executed)
    println!("cargo:rerun-if-changed={}", docs_dir_path.display());
}