Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ stardoc(
symbol_names = [
"rust_doc",
"rust_doc_test",
"rust_workspace_doc",
"rust_workspace_doc_aspect",
],
table_of_contents_template = "@stardoc//stardoc:templates/markdown_tables/table_of_contents.vm",
deps = [":all_docs"],
Expand Down
11 changes: 11 additions & 0 deletions rust/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ load(
"//rust/private:rustdoc_test.bzl",
_rust_doc_test = "rust_doc_test",
)
load(
"//rust/private:rustdoc_workspace.bzl",
_rust_workspace_doc = "rust_workspace_doc",
_rust_workspace_doc_aspect = "rust_workspace_doc_aspect",
)
load(
"//rust/private:rustfmt.bzl",
_rustfmt_aspect = "rustfmt_aspect",
Expand Down Expand Up @@ -108,6 +113,12 @@ rust_doc = _rust_doc
rust_doc_test = _rust_doc_test
# See @rules_rust//rust/private:rustdoc_test.bzl for a complete description.

rust_workspace_doc = _rust_workspace_doc
# See @rules_rust//rust/private:rustdoc_workspace.bzl for a complete description.

rust_workspace_doc_aspect = _rust_workspace_doc_aspect
# See @rules_rust//rust/private:rustdoc_workspace.bzl for a complete description.

clippy_flag = _clippy_flag
clippy_flags = _clippy_flags
# See @rules_rust//rust/private:clippy.bzl for a complete description.
Expand Down
53 changes: 36 additions & 17 deletions rust/private/rustdoc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ def rustdoc_compile_action(
output = None,
rustdoc_flags = [],
is_test = False,
force_depend_on_objects = None):
force_depend_on_objects = None,
attr = None,
file = None,
files = None):
"""Create a struct of information needed for a `rustdoc` compile action based on crate passed to the rustdoc rule.

Args:
Expand All @@ -78,6 +81,12 @@ def rustdoc_compile_action(
is_test (bool, optional): If True, the action will be configured for `rust_doc_test` targets
force_depend_on_objects (bool, optional): If set, overrides is_test for controlling whether
to depend on .rlib files instead of .rmeta. Defaults to is_test.
attr (struct, optional): The attributes to read target info from. Defaults
to `ctx.attr`. Aspects should pass `ctx.rule.attr`.
file (struct, optional): The files struct to read single-file attributes
from. Defaults to `ctx.file`. Aspects should pass `ctx.rule.file`.
files (struct, optional): The files struct to read multi-file attributes
from. Defaults to `ctx.files`. Aspects should pass `ctx.rule.files`.

Returns:
struct: A struct of some `ctx.actions.run` arguments.
Expand All @@ -93,6 +102,13 @@ def rustdoc_compile_action(
expand_directories = False,
)

if attr == None:
attr = ctx.attr
if file == None:
file = ctx.file
if files == None:
files = ctx.files

# Specify rustc flags for lints, if they were provided.
lint_files = []
if lints_info:
Expand All @@ -101,14 +117,14 @@ def rustdoc_compile_action(

# Collect HTML customization files
html_input_files = []
if hasattr(ctx.file, "html_in_header") and ctx.file.html_in_header:
html_input_files.append(ctx.file.html_in_header)
if hasattr(ctx.file, "html_before_content") and ctx.file.html_before_content:
html_input_files.append(ctx.file.html_before_content)
if hasattr(ctx.file, "html_after_content") and ctx.file.html_after_content:
html_input_files.append(ctx.file.html_after_content)
if hasattr(ctx.files, "markdown_css"):
html_input_files.extend(ctx.files.markdown_css)
if hasattr(file, "html_in_header") and file.html_in_header:
html_input_files.append(file.html_in_header)
if hasattr(file, "html_before_content") and file.html_before_content:
html_input_files.append(file.html_before_content)
if hasattr(file, "html_after_content") and file.html_after_content:
html_input_files.append(file.html_after_content)
if hasattr(files, "markdown_css"):
html_input_files.extend(files.markdown_css)

cc_toolchain, feature_configuration = find_cc_toolchain(ctx)

Expand All @@ -120,8 +136,8 @@ def rustdoc_compile_action(

compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs(
ctx = ctx,
file = ctx.file,
files = ctx.files,
file = file,
files = files,
linkstamps = depset([]),
toolchain = toolchain,
cc_toolchain = cc_toolchain,
Expand Down Expand Up @@ -169,8 +185,8 @@ def rustdoc_compile_action(

args, env = construct_arguments(
ctx = ctx,
attr = ctx.attr,
file = ctx.file,
attr = attr,
file = file,
toolchain = toolchain,
tool_path = toolchain.rust_doc.short_path if is_test else toolchain.rust_doc.path,
cc_toolchain = cc_toolchain,
Expand Down Expand Up @@ -201,8 +217,11 @@ def rustdoc_compile_action(
if "OUT_DIR" in env:
env.update({"OUT_DIR": "${{pwd}}/{}".format(build_info.out_dir.short_path)})

# Create the combined inputs including HTML customization files
all_inputs = depset([crate_info.output], transitive = [compile_inputs, depset(html_input_files)])
# Create the combined inputs including HTML customization files. Synthetic
# crates (e.g. the merge-finalize stub of `rust_workspace_doc`) have no
# compiled output to depend on.
direct_inputs = [crate_info.output] if crate_info.output else []
all_inputs = depset(direct_inputs, transitive = [compile_inputs, depset(html_input_files)])

return struct(
executable = ctx.executable._process_wrapper,
Expand All @@ -213,7 +232,7 @@ def rustdoc_compile_action(
tools = [toolchain.rust_doc],
)

def _zip_action(ctx, input_dir, output_zip, crate_label):
def zip_action(ctx, input_dir, output_zip, crate_label):
"""Creates an archive of the generated documentation from `rustdoc`

Args:
Expand Down Expand Up @@ -301,7 +320,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, crate.label)
zip_action(ctx, output_dir, ctx.outputs.rust_doc_zip, crate.label)

return [
DefaultInfo(
Expand Down
8 changes: 8 additions & 0 deletions rust/private/rustdoc/doc_merger/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("//rust/private:rust.bzl", "rust_binary")

rust_binary(
name = "doc_merger",
srcs = ["doc_merger.rs"],
edition = "2024",
visibility = ["//visibility:public"],
)
121 changes: 121 additions & 0 deletions rust/private/rustdoc/doc_merger/doc_merger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::fs;
use std::path::{Path, PathBuf};

const USAGE: &str = r#"usage: doc_merger --output <dir> --inputs <dir>...

Merges multiple rustdoc output directories into a single documentation tree.

Args:
--output: Directory to write the merged documentation to.
--inputs: Rustdoc output directories to copy into the output directory, in
order. Later directories overwrite colliding files from earlier ones, so
the directory produced by `rustdoc --merge=finalize` must be passed last.
"#;

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

struct Args {
output: PathBuf,
inputs: Vec<PathBuf>,
}

fn parse_args() -> Args {
let mut output = None;
let mut inputs = Vec::new();

#[derive(PartialEq)]
enum State {
None,
Output,
Inputs,
}

let mut state = State::None;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--output" => state = State::Output,
"--inputs" => state = State::Inputs,
_ => match state {
State::Output => {
output = Some(PathBuf::from(&arg));
state = State::None;
}
State::Inputs => inputs.push(PathBuf::from(&arg)),
State::None => die!("unexpected argument `{}`\n{}", arg, USAGE),
},
}
}

let output = output.unwrap_or_else(|| die!("missing --output\n{}", USAGE));
if inputs.is_empty() {
die!("missing --inputs\n{}", USAGE);
}

Args { output, inputs }
}

/// Recursively copy the contents of `src` into `dst`, overwriting existing files.
fn copy_tree(src: &Path, dst: &Path) {
let entries = fs::read_dir(src)
.unwrap_or_else(|e| die!("fatal: failed to read directory {}: {}", src.display(), e));
for entry in entries {
let entry = entry
.unwrap_or_else(|e| die!("fatal: failed to read entry in {}: {}", src.display(), e));
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
let file_type = entry
.file_type()
.unwrap_or_else(|e| die!("fatal: failed to stat {}: {}", src_path.display(), e));

// rustdoc leaves a write-only `.lock` flock file in its output
// directory which is not part of the documentation.
if entry.file_name() == ".lock" {
continue;
}

// Tree artifact inputs may be exposed as symlinks; resolve them.
let is_dir = if file_type.is_symlink() {
src_path.is_dir()
} else {
file_type.is_dir()
};

if is_dir {
fs::create_dir_all(&dst_path)
.unwrap_or_else(|e| die!("fatal: failed to create {}: {}", dst_path.display(), e));
copy_tree(&src_path, &dst_path);
} else {
// Copies preserve permissions, so an earlier copy of a read-only
// file must be removed before it can be overwritten.
if let Err(first_error) = fs::copy(&src_path, &dst_path) {
let _ = fs::remove_file(&dst_path);
fs::copy(&src_path, &dst_path).unwrap_or_else(|_| {
die!(
"fatal: failed to copy {} to {}: {}",
src_path.display(),
dst_path.display(),
first_error
)
});
}
}
}
}

fn main() {
let args = parse_args();

fs::create_dir_all(&args.output)
.unwrap_or_else(|e| die!("fatal: failed to create {}: {}", args.output.display(), e));

for input in &args.inputs {
copy_tree(input, &args.output);
}
}
Loading