cargo_docs/
lib.rs

1use cargo::core::compiler::{CompileMode, Executor};
2use cargo::core::{PackageId, Shell, Target, Verbosity, Workspace};
3use cargo::ops::{compile_with_exec, CompileOptions};
4use cargo::util::errors::CargoResult;
5use cargo::util::{homedir, GlobalContext};
6use cargo_util::ProcessBuilder;
7use http::response::Builder as ResponseBuilder;
8use http::{header, StatusCode};
9use hyper::server::conn::http1;
10use hyper::service::service_fn;
11use hyper::{Request, Response};
12use hyper_staticfile::Body;
13use hyper_staticfile::Static;
14use hyper_util::rt::tokio::TokioIo;
15use std::path::PathBuf;
16use std::sync::Arc;
17use tokio::net::TcpListener;
18
19/// run `cargo doc` with extra args
20#[allow(dead_code)]
21pub async fn run_cargo_doc(args: &Vec<String>) -> std::process::ExitStatus {
22    // std::io::Result<> {
23    // async fn main() ->  {
24    let mut cmd = tokio::process::Command::new("cargo");
25    cmd.arg("doc").args(args);
26    let stdcmd = cmd.as_std();
27    log::info!(
28        "Running {} {}",
29        stdcmd.get_program().to_string_lossy(),
30        stdcmd
31            .get_args()
32            .map(|s| s.to_string_lossy().to_string())
33            .collect::<Vec<String>>()
34            .join(" ")
35    );
36    let mut child = tokio::process::Command::new("cargo")
37        .arg("doc")
38        .args(args)
39        .spawn()
40        .expect("failed to run `cargo doc`");
41    child.wait().await.expect("failed to wait")
42}
43
44/// handle crate doc request with redirect on `/`
45///
46/// <https://github.com/stephank/hyper-staticfile/blob/HEAD/examples/doc_server.rs>
47#[allow(dead_code)]
48pub async fn handle_crate_request<B>(
49    req: Request<B>,
50    static_: Static,
51    crate_name: String,
52) -> Result<Response<Body>, std::io::Error> {
53    let target = if let Some(query) = req.uri().query() {
54        format!("/{crate_name}/?{query}")
55    } else {
56        format!("/{crate_name}/")
57    };
58    match req.uri().path() {
59        "/" => Ok(ResponseBuilder::new()
60            .status(StatusCode::FOUND)
61            .header(header::LOCATION, target)
62            .body(Body::Empty)
63            .expect("unable to build response")),
64        _ => static_.clone().serve(req).await,
65    }
66}
67
68/// serve rust book / std doc on `addr`
69#[allow(dead_code)]
70pub async fn serve_rust_doc(addr: &std::net::SocketAddr) -> Result<(), anyhow::Error> {
71    Ok(serve_rustbook(addr).await?)
72}
73
74/// get crate info
75#[allow(dead_code)]
76pub fn get_crate_info(manifest_path: &PathBuf) -> Result<(String, PathBuf), anyhow::Error> {
77    let mut shell = Shell::default();
78    shell.set_verbosity(Verbosity::Quiet);
79    let cwd = std::env::current_dir()?;
80    let cargo_home_dir = homedir(&cwd).expect("Errror locating homedir");
81    let config = GlobalContext::new(shell, cwd, cargo_home_dir);
82    let workspace = Workspace::new(manifest_path, &config).expect("Error making workspace");
83
84    let mut compile_opts = CompileOptions::new(
85        &config,
86        CompileMode::Doc {
87            deps: true,
88            json: false,
89        },
90    )
91    .expect("Making CompileOptions");
92
93    // set to Default, otherwise cargo will complain about virtual manifest:
94    //
95    // https://docs.rs/cargo/latest/src/cargo/core/workspace.rs.html#265-275
96    // https://docs.rs/cargo/latest/src/cargo/ops/cargo_compile.rs.html#125-184
97    compile_opts.spec = cargo::ops::Packages::Default;
98
99    // reference:
100    // https://docs.rs/cargo/latest/src/cargo/ops/cargo_doc.rs.html#18-34
101    /// A `DefaultExecutor` calls rustc without doing anything else. It is Cargo's
102    /// default behaviour.
103    #[derive(Copy, Clone)]
104    struct DefaultExecutor;
105
106    impl Executor for DefaultExecutor {
107        fn exec(
108            &self,
109            _cmd: &ProcessBuilder,
110            _id: PackageId,
111            _target: &Target,
112            _mode: CompileMode,
113            _on_stdout_line: &mut dyn FnMut(&str) -> CargoResult<()>,
114            _on_stderr_line: &mut dyn FnMut(&str) -> CargoResult<()>,
115        ) -> CargoResult<()> {
116            // cmd.exec_with_streaming(on_stdout_line, on_stderr_line, false).map(drop)
117            Ok(())
118        }
119    }
120
121    let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
122    let compilation = compile_with_exec(&workspace, &compile_opts, &exec)?;
123    let root_crate_names = &compilation.root_crate_names;
124    let crate_doc_dir = workspace.target_dir().join("doc").into_path_unlocked();
125    let crate_name = root_crate_names
126        .get(0)
127        .ok_or_else(|| anyhow::anyhow!("no crates with documentation"))?;
128    Ok((crate_name.to_string(), crate_doc_dir))
129}
130
131/// serve crate doc on `addr`
132#[allow(dead_code)]
133pub async fn serve_crate_doc(
134    manifest_path: &PathBuf,
135    addr: &std::net::SocketAddr,
136) -> Result<(), anyhow::Error> {
137    let (crate_name, crate_doc_dir) = get_crate_info(manifest_path)?;
138    let crate_doc_dir = Static::new(crate_doc_dir.clone());
139    let crate_name = crate_name.clone();
140    let handler =
141        service_fn(move |req| handle_crate_request(req, crate_doc_dir.clone(), crate_name.clone()));
142
143    let listener = TcpListener::bind(addr)
144        .await
145        .expect("Failed to create TCP listener");
146
147    loop {
148        let (tcp, _) = listener.accept().await?;
149        let io = TokioIo::new(tcp);
150        let service = handler.clone();
151        tokio::task::spawn(async move {
152            if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
153                println!("Failed to serve connection: {:?}", err);
154            }
155        });
156    }
157}
158
159/// find rust book location
160///
161/// Some("/home/aaron/.rustup/toolchains/nightly-2021-12-13-x86_64-unknown-linux-gnu/share/doc/rust/html")
162pub fn find_rustdoc() -> Option<PathBuf> {
163    let output = std::process::Command::new("rustup")
164        .arg("which")
165        .arg("rustdoc")
166        .output()
167        .ok()?;
168    if output.status.success() {
169        Some(PathBuf::from(String::from_utf8(output.stdout).ok()?))
170    } else {
171        None
172    }
173    .and_then(|rustdoc| {
174        Some(
175            rustdoc
176                .parent()?
177                .parent()?
178                .join("share")
179                .join("doc")
180                .join("rust")
181                .join("html"),
182        )
183    })
184}
185
186/// static request handler
187///
188/// <https://github.com/stephank/hyper-staticfile/blob/HEAD/examples/doc_server.rs>
189#[allow(dead_code)]
190pub async fn handle_request<B>(
191    req: Request<B>,
192    static_: Static,
193) -> Result<Response<Body>, std::io::Error> {
194    static_.clone().serve(req).await
195}
196
197/// serve rust book on `addr`
198#[allow(dead_code)]
199pub async fn serve_rustbook(addr: &std::net::SocketAddr) -> Result<(), anyhow::Error> {
200    let rustdoc_dir = find_rustdoc().expect("Error locating rustdoc");
201    Ok(serve_dir(&rustdoc_dir, addr).await?)
202}
203
204/// serve `dir` on `addr`
205#[allow(dead_code)]
206pub async fn serve_dir(dir: &PathBuf, addr: &std::net::SocketAddr) -> Result<(), anyhow::Error> {
207    let dir = Static::new(dir.clone());
208    let handler = service_fn(move |req| handle_request(req, dir.clone()));
209
210    let listener = TcpListener::bind(addr)
211        .await
212        .expect("Failed to create TCP listener");
213
214    loop {
215        let (tcp, _) = listener.accept().await?;
216        let io = TokioIo::new(tcp);
217        let service = handler.clone();
218        tokio::task::spawn(async move {
219            if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
220                println!("Failed to serve connection: {:?}", err);
221            }
222        });
223    }
224}