Initial commit
This commit is contained in:
commit
4b5ab8f0ac
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
3428
Cargo.lock
generated
Normal file
3428
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "opendocs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
axum = { version = "0.6.20", features = ["tracing"] }
|
||||
cargo = "0.73.1"
|
||||
clap = { version = "4.4.4", features = ["derive"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
tokio = { version = "1.32.0", features = ["net", "rt", "macros", "rt-multi-thread", "process", "io-util"] }
|
||||
tower = "0.4.13"
|
||||
tower-http = { version = "0.4.4", features = ["fs", "tracing", "trace"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.17"
|
83
index.html
Normal file
83
index.html
Normal file
@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Rust Docs</title>
|
||||
<meta charset="utf-8">
|
||||
<!-- Make the device look good on phones -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: black;
|
||||
}
|
||||
|
||||
#links {
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#main {
|
||||
position:fixed;
|
||||
left: 0;
|
||||
bottom: 0;right: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 2.5rem);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="links">
|
||||
<a href="std/std/" id="std-docs" target="main" onclick="focusFrame()">STD</a>
|
||||
<a href="local/tracing/" id="local-docs" target="main" onclick="focusFrame()">Cargo</a>
|
||||
</div>
|
||||
|
||||
<iframe name="main" id="main" src="local/tracing/"></iframe>
|
||||
</body>
|
||||
|
||||
<script type="text/javascript">
|
||||
let links = {
|
||||
"std": document.getElementById("std-docs"),
|
||||
"local": document.getElementById("local-docs"),
|
||||
};
|
||||
let frame = document.getElementById("main");
|
||||
let lastLoaded = "local";
|
||||
|
||||
function toggleLastLoaded() {
|
||||
if (lastLoaded === "std") {
|
||||
lastLoaded = "local";
|
||||
} else {
|
||||
lastLoaded = "std";
|
||||
}
|
||||
links[lastLoaded].click();
|
||||
}
|
||||
|
||||
function focusFrame() {
|
||||
frame.contentWindow.focus();
|
||||
frame.addEventListener("load", function() {
|
||||
if (this.contentWindow.window.searchState) {
|
||||
this.contentWindow.window.searchState.focus();
|
||||
}
|
||||
frame.contentWindow.removeEventListener("keydown", cl);
|
||||
frame.contentWindow.addEventListener("keydown", cl);
|
||||
});
|
||||
}
|
||||
|
||||
function cl(e) {
|
||||
if ((e.which == 219 || e.which == 221) && !e.ctrlKey && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
toggleLastLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
focusFrame();
|
||||
</script>
|
||||
</html>
|
187
src/main.rs
Normal file
187
src/main.rs
Normal file
@ -0,0 +1,187 @@
|
||||
#![feature(try_blocks)]
|
||||
use anyhow::bail;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use axum::http::header::CONTENT_SECURITY_POLICY;
|
||||
use axum::http::header::X_FRAME_OPTIONS;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::response::Html;
|
||||
use axum::routing::get;
|
||||
use clap::Parser;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::ffi::OsStr;
|
||||
use std::ffi::OsString;
|
||||
use std::os::unix::prelude::OsStringExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinHandle;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use axum::Router;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(default_values = ["--all"])]
|
||||
doc_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
info!("Getting standard docs path");
|
||||
let std_docs_path = get_std_docs_path().await?;
|
||||
info!("Found: {std_docs_path:?}");
|
||||
info!("Building docs...");
|
||||
let build_docs_path = build_docs().await?;
|
||||
info!("Done: {build_docs_path:?}");
|
||||
|
||||
start_http(std_docs_path, build_docs_path).await
|
||||
}
|
||||
|
||||
async fn start_http(std_docs_path: PathBuf, build_docs_path: PathBuf) -> Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/", get(get_index))
|
||||
.nest_service("/local", ServeDir::new(build_docs_path))
|
||||
.nest_service("/std", ServeDir::new(std_docs_path))
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
let addr = SocketAddr::from_str("127.0.0.1:8888").unwrap();
|
||||
info!("Listening on address {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_index() -> (HeaderMap, Html<&'static [u8]>) {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(X_FRAME_OPTIONS, "SAMEORIGIN".parse().unwrap());
|
||||
headers.insert(CONTENT_SECURITY_POLICY, "child-src 'self'".parse().unwrap());
|
||||
(headers, Html(include_bytes!("../index.html")))
|
||||
}
|
||||
|
||||
async fn build_docs() -> Result<PathBuf> {
|
||||
// let options = DocOptions {
|
||||
// open_result: false,
|
||||
// compile_opts: CompileOptions {
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
let mut child = Command::new("cargo")
|
||||
.args(["doc", "--keep-going", "--message-format=json"])
|
||||
.stdout(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.context("Could not spawn cargo doc")?;
|
||||
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.context("Stdout could not be taken from Command")?;
|
||||
let join_handle: JoinHandle<Result<Result<PathBuf>>> = tokio::spawn(async move {
|
||||
let mut lines = BufReader::new(stdout).lines();
|
||||
let result = try {
|
||||
let mut latest_artifact = None;
|
||||
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
let response = serde_json::from_str::<CargoDocLine>(&line)
|
||||
.context("Failed to deserialize cargo output")?;
|
||||
|
||||
// info!("Deserialized response: {response:?}");
|
||||
match response {
|
||||
CargoDocLine::CompilerArtifact { filenames } => {
|
||||
// info!("Got artifacts: {filenames:#?}");
|
||||
latest_artifact = filenames
|
||||
.into_iter()
|
||||
.find(|a| a.file_name() == Some(OsStr::new("index.html")))
|
||||
.or(latest_artifact);
|
||||
// info!("Latest artifact: {latest_artifact:?}");
|
||||
}
|
||||
CargoDocLine::BuildFinished { success: false } => {
|
||||
// TODO: Maybe we should stop on some failures
|
||||
|
||||
return Ok(latest_artifact.context("No artifacts generated"));
|
||||
|
||||
// bail!("Cargo doc did not complete successfully");
|
||||
}
|
||||
CargoDocLine::BuildFinished { success: true } => {
|
||||
return Ok(latest_artifact.context("No artifacts generated"));
|
||||
}
|
||||
CargoDocLine::BuildScriptExecuted(_) | CargoDocLine::CompilerMessage(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Never got build-finished reason")
|
||||
};
|
||||
|
||||
// Continue reading stdout so cargo doc doesn't crash
|
||||
// Errors can be ignored; this is just for draining stdout
|
||||
while let Ok(Some(_line)) = lines.next_line().await {}
|
||||
|
||||
result
|
||||
});
|
||||
|
||||
let _status = child.wait().await.context("Cargo doc runtime error")?;
|
||||
|
||||
let output_path = join_handle
|
||||
.await
|
||||
.context("Failed to join reader handle")???;
|
||||
// info!("with output path: {:?}", output_path);
|
||||
|
||||
let output_path = output_path
|
||||
.parent()
|
||||
.map(|p| p.parent())
|
||||
.flatten()
|
||||
.context("Crate docs directory is invalid")?;
|
||||
|
||||
if !output_path.is_dir() {
|
||||
bail!("Crate docs directory does not exist");
|
||||
}
|
||||
|
||||
Ok(output_path.to_owned())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "reason")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
enum CargoDocLine {
|
||||
CompilerArtifact { filenames: Vec<PathBuf> },
|
||||
BuildFinished { success: bool },
|
||||
BuildScriptExecuted(Value),
|
||||
CompilerMessage(Value),
|
||||
}
|
||||
|
||||
async fn get_std_docs_path() -> Result<PathBuf> {
|
||||
// rustup docs --path --std
|
||||
let path = PathBuf::from(OsString::from_vec(
|
||||
Command::new("rustup")
|
||||
.args(["docs", "--path", "--std"])
|
||||
.output()
|
||||
.await
|
||||
.context("Could not spawn cargo doc")?
|
||||
.stdout,
|
||||
));
|
||||
let path = path
|
||||
.parent()
|
||||
.map(|p| p.parent())
|
||||
.flatten()
|
||||
.context("std docs directory is invalid")?;
|
||||
|
||||
if !path.is_dir() {
|
||||
bail!("std docs directory does not exist");
|
||||
}
|
||||
|
||||
Ok(path.to_owned())
|
||||
}
|
Loading…
Reference in New Issue
Block a user