xtask: also generate EXAMPLES section

Include "Examples" section from READMEs if they exist into the generated
manual page.

Signed-off-by: Manos Pitsidianakis <manos.pitsidianakis@linaro.org>
This commit is contained in:
Manos Pitsidianakis 2025-03-04 10:08:24 +02:00 committed by Viresh Kumar
parent 72f811cf4f
commit f6f4a90e12
4 changed files with 80 additions and 11 deletions

16
Cargo.lock generated
View File

@ -857,6 +857,15 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "markdown"
version = "1.0.0-alpha.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9047e0a37a596d4e15411a1ffbdabe71c328908cb90a721cb9bf8dcf3434e6d2"
dependencies = [
"unicode-id",
]
[[package]]
name = "memchr"
version = "2.7.4"
@ -1741,6 +1750,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "unicode-id"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
[[package]]
name = "unicode-ident"
version = "1.0.18"
@ -2350,6 +2365,7 @@ version = "0.1.0"
dependencies = [
"clap",
"clap_mangen",
"markdown",
"toml 0.8.19",
]

View File

@ -13,6 +13,7 @@ publish = false
clap = { version = "4.5", features = ["derive"], optional = true }
clap_mangen = { version = "0.2.24", optional = true }
toml = { version = "0.8.19", optional = true }
markdown = { version = "=1.0.0-alpha.23", optional = true }
[build-dependencies]
@ -22,7 +23,7 @@ vhost-device-scmi = []
vhost-device-sound = ["vhost-device-sound-alsa", "vhost-device-sound-pipewire"]
vhost-device-sound-alsa = ["mangen"]
vhost-device-sound-pipewire = ["mangen"]
mangen = ["dep:clap_mangen", "dep:clap", "dep:toml"]
mangen = ["dep:clap_mangen", "dep:clap", "dep:toml", "dep:markdown"]
[lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(feature, values("alsa-backend", "pw-backend"))'] }

View File

@ -6,6 +6,8 @@ This binary crate provides support for running useful tasks with `cargo xtask <.
The `mangen` task which is enabled by the `mangen` cargo feature, builds ROFF manual pages for binary crates in this repository. It uses the [`clap_mangen`](https://crates.io/crates/clap_mangen) crate to generate ROFF from the crate's argument types which implement the `clap::CommandFactory` trait, through the `clap::Parser` derive macro.
Furthmore, if the `README.md` of a crate contains an `Examples` heading, it includes it in the manual page.
```session
$ cargo xtask mangen
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s

View File

@ -15,6 +15,8 @@ use clap::CommandFactory;
#[cfg(feature = "mangen")]
use clap_mangen::Man;
#[cfg(feature = "mangen")]
use markdown::{to_mdast, ParseOptions};
#[cfg(feature = "mangen")]
use toml::value::Table;
// Use vhost-device-sound's args module as our own using the #[path] attribute
@ -63,7 +65,19 @@ fn print_help() {
}
#[cfg(feature = "mangen")]
fn mangen_for_crate<T: CommandFactory>(manifest: Table) -> Result<Vec<u8>, Box<dyn Error>> {
fn mangen_for_crate<T: CommandFactory>(
crate_dir: std::path::PathBuf,
) -> Result<Vec<u8>, Box<dyn Error>> {
let readme_md = std::fs::read_to_string(crate_dir.join("README.md"))?;
let example_text = parse_examples_from_readme(readme_md).unwrap_or_default();
let examples = if example_text.is_empty() {
None
} else {
Some(example_text.trim())
};
let manifest = std::fs::read_to_string(crate_dir.join("Cargo.toml"))?;
let manifest = manifest.as_str().parse::<Table>()?;
let name: &'static str = manifest["package"]["name"]
.as_str()
.unwrap()
@ -94,6 +108,14 @@ fn mangen_for_crate<T: CommandFactory>(manifest: Table) -> Result<Vec<u8>, Box<d
let man = Man::new(cmd);
let mut buffer: Vec<u8> = Default::default();
man.render(&mut buffer)?;
if let Some(examples) = examples {
let mut examples_section = clap_mangen::roff::Roff::new();
examples_section.control("SH", ["EXAMPLES"]);
for line in examples.lines() {
examples_section.text(vec![line.into()]);
}
examples_section.to_writer(&mut buffer)?;
}
clap_mangen::roff::Roff::new()
.control("SH", ["REPORTING BUGS"])
.text(vec![format!(
@ -105,6 +127,41 @@ fn mangen_for_crate<T: CommandFactory>(manifest: Table) -> Result<Vec<u8>, Box<d
Ok(buffer)
}
#[cfg(feature = "mangen")]
fn parse_examples_from_readme(readme_md: String) -> Result<String, Box<dyn Error>> {
use markdown::mdast;
let mdast = to_mdast(&readme_md, &ParseOptions::gfm()).map_err(|err| err.to_string())?;
let mut example_text = String::new();
if let mdast::Node::Root(root) = mdast {
if let Some(examples_index) = root.children.iter().position(|r| matches!(r, mdast::Node::Heading(mdast::Heading { ref children, .. }) if matches!(children.first(), Some(mdast::Node::Text(mdast::Text { ref value, .. })) if value.trim() == "Examples"))){
let mdast::Node::Heading(examples_heading) =
&root.children[examples_index]
else {
// SAFETY: Unreachable because we found the exact position earlier.
unreachable!();
};
let depth = examples_heading.depth;
let mut i = examples_index + 1;
while i < root.children.len() && !matches!(root.children[i], mdast::Node::Heading(ref h) if h.depth >= depth) {
match &root.children[i] {
mdast::Node::Paragraph(p) => {
example_text.push_str(&p.children.iter().map(|t| t.to_string()).collect::<Vec<String>>().join(" "));
example_text.push_str("\n\n");
},
mdast::Node::Code(c) => {
example_text.push_str(&c.value);
example_text.push_str("\n\n");
},
_ => {},
}
i += 1;
}
}
}
Ok(example_text)
}
#[cfg(feature = "mangen")]
fn mangen() -> Result<(), Box<dyn Error>> {
let workspace_dir = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"))
@ -124,11 +181,7 @@ fn mangen() -> Result<(), Box<dyn Error>> {
{
use vhost_device_sound::SoundArgs;
let manifest =
std::fs::read_to_string(workspace_dir.join("vhost-device-sound/Cargo.toml"))?;
let manifest = manifest.as_str().parse::<Table>()?;
let buffer = mangen_for_crate::<SoundArgs>(manifest)?;
let buffer = mangen_for_crate::<SoundArgs>(workspace_dir.join("vhost-device-sound"))?;
let man_path = dist_dir.join("vhost-device-sound.1");
buffers.push((man_path, buffer));
}
@ -136,10 +189,7 @@ fn mangen() -> Result<(), Box<dyn Error>> {
{
use vhost_device_scmi::ScmiArgs;
let manifest = std::fs::read_to_string(workspace_dir.join("vhost-device-scmi/Cargo.toml"))?;
let manifest = manifest.as_str().parse::<Table>()?;
let buffer = mangen_for_crate::<ScmiArgs>(manifest)?;
let buffer = mangen_for_crate::<ScmiArgs>(workspace_dir.join("vhost-device-scmi"))?;
let man_path = dist_dir.join("vhost-device-scmi.1");
buffers.push((man_path, buffer));
}