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

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));
}