Initial commit

This commit is contained in:
Austen Adler 2023-10-06 17:59:15 -04:00
commit 4b5ab8f0ac
5 changed files with 3718 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

3428
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
Cargo.toml Normal file
View 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
View 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
View 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())
}