Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rust_doc: add web server for docs #475

Closed
wants to merge 16 commits into from
7 changes: 7 additions & 0 deletions docs/docs_repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ def repositories(is_top_level = False):
sha256 = "5d7191bb0800434a9192d8ac80cba4909e96dbb087c5d51f168fedd7bde7b525",
strip_prefix = "stardoc-1ef781ced3b1443dca3ed05dec1989eca1a4e1cd",
)

maybe(
http_archive,
name = "rules_python",
url = "https://github.com/bazelbuild/rules_python/releases/download/0.1.0/rules_python-0.1.0.tar.gz",
sha256 = "b6d46438523a3ec0f3cead544190ee13223a52f6a6765a29eae7b7cc24cc83a0",
)
6 changes: 6 additions & 0 deletions examples/hello_world/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ load(
"@io_bazel_rules_rust//rust:rust.bzl",
"rust_binary",
"rust_doc",
"rust_doc_server",
)

package(default_visibility = ["//visibility:public"])
Expand All @@ -16,3 +17,8 @@ rust_doc(
name = "hello_world_doc",
dep = ":hello_world",
)

rust_doc_server(
name = "hello_world_doc_server",
dep = ":hello_world_doc",
)
1 change: 1 addition & 0 deletions rust/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
package(default_visibility = ["//visibility:public"])

exports_files([
"doc_server.template.py",
"known_shas.bzl",
"repositories.bzl",
"rust.bzl",
Expand Down
89 changes: 89 additions & 0 deletions rust/doc_server.template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import argparse
import errno
import mimetypes
import os
import sys
from wsgiref import simple_server
import zipfile


ZIP_FILE = "{ZIP_FILE}"
TARGET_PATH = "{TARGET_PATH}"
CRATE_NAME = "{CRATE_NAME}"

DEFAULT_PORT = 8000


def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--host",
type=str,
default="",
help="start web server on this host (default: %(default)r)",
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="start web server on this port; pass 0 to automatically "
"select a free port (default: %(default)s)",
)
args = parser.parse_args()
port = args.port

webfiles = os.path.join(os.path.dirname(__file__), ZIP_FILE)
data = {}
with open(webfiles, "rb") as fp:
with zipfile.ZipFile(fp) as zp:
for path in zp.namelist():
data[path] = zp.read(path)
sys.stderr.write("Read %d files from %s\n" % (len(data), ZIP_FILE))

default_path = "/%s/%s/index.html" % (TARGET_PATH, CRATE_NAME)

def app(environ, start_response):
p = environ.get("PATH_INFO", "/").lstrip("/")
if not p:
start_response("302 Found", [("Location", default_path)])
yield b"302 Found\n"
return
if p.endswith("/"):
p += "index.html"
blob = data.get(p)
if not blob:
start_response("404 Not Found", [])
yield b"404 Not Found\n"
return
(mime_type, encoding) = mimetypes.guess_type(p)
headers = []
if mime_type is not None:
headers.append(("Content-Type", mime_type))
if encoding is not None:
headers.append(("Content-Encoding", encoding))
start_response("200 OK", headers)
yield blob

try:
server = simple_server.make_server("", port, app)
except OSError as e:
if e.errno != getattr(errno, "EADDRINUSE", 0):
raise
sys.stderr.write("%s\n" % e)
sys.stderr.write(
"fatal: failed to bind to port %d; try setting a --port argument\n"
% port
)
sys.exit(1)
# Find which port was actually bound, in case user requested port 0.
real_port = server.socket.getsockname()[1]
msg = "Serving %s docs on port %d\n" % (CRATE_NAME, real_port)
sys.stderr.write(msg)
try:
server.serve_forever()
except KeyboardInterrupt:
print()


if __name__ == "__main__":
main()
80 changes: 76 additions & 4 deletions rust/private/rustdoc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
# buildifier: disable=module-docstring
load("@io_bazel_rules_rust//rust:private/rustc.bzl", "CrateInfo", "DepInfo", "add_crate_link_flags", "add_edition_flags")
load("@io_bazel_rules_rust//rust:private/utils.bzl", "find_toolchain")
load("@rules_python//python:defs.bzl", "py_binary")

_DocInfo = provider(
doc = "A provider containing information about a Rust documentation target.",
fields = {
"zip_file": "File: the zip file with rustdoc(1) output",
},
)

_rust_doc_doc = """Generates code documentation.

Expand Down Expand Up @@ -66,6 +74,7 @@ def _rust_doc_impl(ctx):

crate = ctx.attr.dep[CrateInfo]
dep_info = ctx.attr.dep[DepInfo]
doc_info = _DocInfo(zip_file = ctx.outputs.rust_doc_zip)

toolchain = find_toolchain(ctx)

Expand Down Expand Up @@ -109,6 +118,7 @@ def _rust_doc_impl(ctx):

# This rule does nothing without a single-file output, though the directory should've sufficed.
_zip_action(ctx, output_dir, ctx.outputs.rust_doc_zip)
return [crate, doc_info]

def _zip_action(ctx, input_dir, output_zip):
"""Creates an archive of the generated documentation from `rustdoc`
Expand All @@ -119,15 +129,16 @@ def _zip_action(ctx, input_dir, output_zip):
output_zip (File): The location of the output archive containing generated documentation
"""
args = ctx.actions.args()

# Create but not compress.
args.add("c", output_zip)
args.add(ctx.executable._zipper)
args.add(output_zip)
args.add(ctx.bin_dir.path)
args.add_all([input_dir], expand_directories = True)
ctx.actions.run(
executable = ctx.executable._zipper,
executable = ctx.executable._dir_zipper,
inputs = [input_dir],
outputs = [output_zip],
arguments = [args],
tools = [ctx.executable._zipper],
)

rust_doc = rule(
Expand Down Expand Up @@ -159,6 +170,11 @@ rust_doc = rule(
doc = "File to add in `<body>`, after content.",
allow_single_file = [".html", ".md"],
),
"_dir_zipper": attr.label(
default = Label("//util/dir_zipper"),
cfg = "exec",
executable = True,
),
"_zipper": attr.label(
default = Label("@bazel_tools//tools/zip:zipper"),
cfg = "exec",
Expand All @@ -170,3 +186,59 @@ rust_doc = rule(
},
toolchains = ["@io_bazel_rules_rust//rust:toolchain"],
)

def _rust_doc_server_stub_impl(ctx):
dep = ctx.attr.rust_doc_dep
crate_name = dep[CrateInfo].name
zip_file = dep[_DocInfo].zip_file
path_parts = [dep.label.workspace_root, dep.label.package, dep.label.name]
target_path = "/".join([p for p in path_parts if p])
ctx.actions.expand_template(
template = ctx.file._server_template,
output = ctx.outputs.main,
substitutions = {
"{TARGET_PATH}": target_path,
"{CRATE_NAME}": crate_name,
"{ZIP_FILE}": zip_file.basename,
},
)

_rust_doc_server_stub = rule(
implementation = _rust_doc_server_stub_impl,
attrs = {
"rust_doc_dep": attr.label(
mandatory = True,
providers = [CrateInfo, _DocInfo],
),
"main": attr.output(),
"zip_file": attr.output(),
"_server_template": attr.label(
default = Label("//rust:doc_server.template.py"),
allow_single_file = True,
),
},
)

def rust_doc_server(name, dep, **kwargs):
"""Generates a web server to display code documentation.

Args:
name: A unique name for this target.
dep: Label for a `rust_doc` rule whose docs to serve.
**kwargs: Any generic binary kwargs, like `tags` or `visibility`.
"""
python_stub_name = name + "_python_stub"
python_stub_output = name + ".py"
zip_file = dep + ".zip"
_rust_doc_server_stub(
name = python_stub_name,
rust_doc_dep = dep,
main = python_stub_output,
)
py_binary(
name = name,
srcs = [python_stub_output],
data = [zip_file],
srcs_version = "PY3",
python_version = "PY3",
)
7 changes: 7 additions & 0 deletions rust/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def rust_repositories(
type = "zip",
)

maybe(
http_archive,
name = "rules_python",
url = "https://github.com/bazelbuild/rules_python/releases/download/0.1.0/rules_python-0.1.0.tar.gz",
sha256 = "b6d46438523a3ec0f3cead544190ee13223a52f6a6765a29eae7b7cc24cc83a0",
)

maybe(
http_archive,
name = "bazel_skylib",
Expand Down
4 changes: 4 additions & 0 deletions rust/rust.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ load(
load(
"@io_bazel_rules_rust//rust:private/rustdoc.bzl",
_rust_doc = "rust_doc",
_rust_doc_server = "rust_doc_server",
)
load(
"@io_bazel_rules_rust//rust:private/rustdoc_test.bzl",
Expand Down Expand Up @@ -53,6 +54,9 @@ rust_benchmark = _rust_benchmark
rust_doc = _rust_doc
# See @io_bazel_rules_rust//rust:private/rustdoc.bzl for a complete description.

rust_doc_server = _rust_doc_server
# See @io_bazel_rules_rust//rust:private/rustdoc.bzl for a complete description.

rust_doc_test = _rust_doc_test
# See @io_bazel_rules_rust//rust:private/rustdoc_test.bzl for a complete description.

Expand Down
8 changes: 8 additions & 0 deletions util/dir_zipper/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("//rust:rust.bzl", "rust_binary")

rust_binary(
name = "dir_zipper",
srcs = ["dir_zipper.rs"],
edition = "2018",
visibility = ["//visibility:public"],
)
77 changes: 77 additions & 0 deletions util/dir_zipper/dir_zipper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;

const USAGE: &str = r#"usage: dir_zipper <zipper> <output> <root-dir> [<file>...]

Creates a zip archive, stripping a directory prefix from each file name.

Args:
zipper: Path to @bazel_tools//tools/zip:zipper.
output: Path to zip file to create: e.g., "/tmp/out.zip".
root_dir: Directory to strip from each archive name, with no trailing
slash: e.g., "/tmp/myfiles".
files: List of files to include in the archive, all under `root_dir`:
e.g., ["/tmp/myfiles/a", "/tmp/myfiles/b/c"].

Example:
dir_zipper \
bazel-rules_rust/external/bazel_tools/tools/zip/zipper/zipper \
/tmp/out.zip \
/tmp/myfiles \
/tmp/myfiles/a /tmp/myfiles/b/c

This will create /tmp/out.zip with file entries "a" and "b/c".
"#;

macro_rules! die {
($($arg:tt)*) => {
{
eprintln!($($arg)*);
std::process::exit(1);
}
};
}

fn main() {
let mut args = std::env::args_os().skip(1);
let (zipper, output, root_dir) = match args.next().zip(args.next()).zip(args.next()) {
Some(((zipper, output), root_dir)) => (
PathBuf::from(zipper),
PathBuf::from(output),
PathBuf::from(root_dir),
),
_ => {
die!("{}", USAGE);
}
};
let files = args.map(PathBuf::from).collect::<Vec<_>>();
let mut comm = Command::new(zipper);
comm.arg("c"); // create, but don't compress
comm.arg(output);
for f in files {
let rel = f.strip_prefix(&root_dir).unwrap_or_else(|_e| {
die!(
"fatal: non-descendant: {} not under {}",
f.display(),
root_dir.display()
);
});
let mut spec = OsString::new();
spec.push(rel);
spec.push("=");
spec.push(f);
comm.arg(spec);
}
let exit_status = comm
.spawn()
.unwrap_or_else(|e| die!("fatal: could not spawn zipper: {}", e))
.wait()
.unwrap_or_else(|e| die!("fatal: could not wait on zipper: {}", e));
if !exit_status.success() {
match exit_status.code() {
Some(c) => std::process::exit(c),
None => die!("fatal: zipper terminated by signal"),
}
}
}