From f035c167577283fb9e775ef47a72a0f4900700a3 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 7 Dec 2024 10:01:44 +0200 Subject: [PATCH 1/8] sound: move CLI arg types to lib submodule Currently, the main CLI struct type is defined in src/main.rs thus inaccessible when using the crate as a library. Move it to its own module under src/args.rs This will be useful for followup commits that will use src/args.rs directly to generate manual pages using clap_mangen Signed-off-by: Manos Pitsidianakis --- vhost-device-sound/src/args.rs | 28 +++++++++++++++++ vhost-device-sound/src/lib.rs | 21 +++++++------ vhost-device-sound/src/main.rs | 55 ++++++++-------------------------- 3 files changed, 51 insertions(+), 53 deletions(-) create mode 100644 vhost-device-sound/src/args.rs diff --git a/vhost-device-sound/src/args.rs b/vhost-device-sound/src/args.rs new file mode 100644 index 000000000..7cd51d5e2 --- /dev/null +++ b/vhost-device-sound/src/args.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause +//! An arguments type for the binary interface of this library. + +use std::path::PathBuf; + +use clap::{Parser, ValueEnum}; + +#[derive(Parser, Debug)] +#[clap(version, about, long_about = None)] +pub struct SoundArgs { + /// vhost-user Unix domain socket path. + #[clap(long)] + pub socket: PathBuf, + /// audio backend to be used + #[clap(long)] + #[clap(value_enum)] + pub backend: BackendType, +} + +#[derive(ValueEnum, Clone, Copy, Default, Debug, Eq, PartialEq)] +pub enum BackendType { + #[default] + Null, + #[cfg(all(feature = "pw-backend", target_env = "gnu"))] + Pipewire, + #[cfg(all(feature = "alsa-backend", target_env = "gnu"))] + Alsa, +} diff --git a/vhost-device-sound/src/lib.rs b/vhost-device-sound/src/lib.rs index 9235b36e9..ea0ec7583 100644 --- a/vhost-device-sound/src/lib.rs +++ b/vhost-device-sound/src/lib.rs @@ -37,6 +37,7 @@ pub fn init_logger() { let _ = env_logger::builder().is_test(true).try_init(); } +pub mod args; pub mod audio_backends; pub mod device; pub mod stream; @@ -50,7 +51,7 @@ use std::{ sync::Arc, }; -use clap::ValueEnum; +pub use args::BackendType; pub use stream::Stream; use thiserror::Error as ThisError; use vhost_user_backend::{VhostUserDaemon, VringRwLock, VringT}; @@ -191,16 +192,6 @@ impl From for Error { } } -#[derive(ValueEnum, Clone, Copy, Default, Debug, Eq, PartialEq)] -pub enum BackendType { - #[default] - Null, - #[cfg(all(feature = "pw-backend", target_env = "gnu"))] - Pipewire, - #[cfg(all(feature = "alsa-backend", target_env = "gnu"))] - Alsa, -} - #[derive(Debug, PartialEq, Eq)] pub struct InvalidControlMessage(u32); @@ -263,6 +254,14 @@ pub struct SoundConfig { audio_backend: BackendType, } +impl From for SoundConfig { + fn from(cmd_args: args::SoundArgs) -> Self { + let args::SoundArgs { socket, backend } = cmd_args; + + Self::new(socket, false, backend) + } +} + impl SoundConfig { /// Create a new instance of the SoundConfig struct, containing the /// parameters to be fed into the sound-backend server. diff --git a/vhost-device-sound/src/main.rs b/vhost-device-sound/src/main.rs index 28d48bf11..283f80204 100644 --- a/vhost-device-sound/src/main.rs +++ b/vhost-device-sound/src/main.rs @@ -1,35 +1,14 @@ // Manos Pitsidianakis // Stefano Garzarella // SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause -use std::{convert::TryFrom, path::PathBuf}; use clap::Parser; -use vhost_device_sound::{start_backend_server, BackendType, Error, Result, SoundConfig}; - -#[derive(Parser, Debug)] -#[clap(version, about, long_about = None)] -struct SoundArgs { - /// vhost-user Unix domain socket path. - #[clap(long)] - socket: PathBuf, - /// audio backend to be used - #[clap(long)] - #[clap(value_enum)] - backend: BackendType, -} - -impl TryFrom for SoundConfig { - type Error = Error; - - fn try_from(cmd_args: SoundArgs) -> Result { - Ok(SoundConfig::new(cmd_args.socket, false, cmd_args.backend)) - } -} +use vhost_device_sound::{args::SoundArgs, start_backend_server, SoundConfig}; fn main() { env_logger::init(); - let config = SoundConfig::try_from(SoundArgs::parse()).unwrap(); + let config = SoundConfig::from(SoundArgs::parse()); loop { start_backend_server(config.clone()); @@ -38,19 +17,14 @@ fn main() { #[cfg(test)] mod tests { + use std::path::{Path, PathBuf}; + + use clap::Parser; use rstest::*; + use vhost_device_sound::BackendType; use super::*; - impl SoundArgs { - fn from_args(socket: PathBuf) -> Self { - SoundArgs { - socket, - backend: BackendType::default(), - } - } - } - fn init_logger() { std::env::set_var("RUST_LOG", "trace"); let _ = env_logger::builder().is_test(true).try_init(); @@ -59,15 +33,15 @@ mod tests { #[test] fn test_sound_config_setup() { init_logger(); - let args = SoundArgs::from_args(PathBuf::from("/tmp/vhost-sound.socket")); + let args = SoundArgs { + socket: PathBuf::from("/tmp/vhost-sound.socket"), + backend: BackendType::default(), + }; + let config = SoundConfig::from(args); - let config = SoundConfig::try_from(args); - assert!(config.is_ok()); - - let config = config.unwrap(); assert_eq!( config.get_socket_path(), - PathBuf::from("/tmp/vhost-sound.socket") + Path::new("/tmp/vhost-sound.socket") ); } @@ -90,10 +64,7 @@ mod tests { backend_name, ]); - let config = SoundConfig::try_from(args); - assert!(config.is_ok()); - - let config = config.unwrap(); + let config = SoundConfig::from(args); assert_eq!(config.get_audio_backend(), backend); } } From 72959c2e9d00a2a0e8dd571894f9fea2c50703c2 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 7 Dec 2024 10:45:35 +0200 Subject: [PATCH 2/8] Add an xtask crate to generate manpages For information about the cargo xtask workflow pattern see: This commit adds an xtask crate with only one task, "mangen", which generates a ROFF manual page under `target/dist/man` directory for the vhost-device-sound binary. The xtask crate can be configured using cargo features, to help packagers configure what manpages are generated. This generates a manpage in target/dist/man/vhost-device-sound.1 The rendered ROFF output looks like: vhost-device-sound(1) General Commands Manual vhost-device-sound(1) NAME vhost-device-sound - A virtio-sound device using the vhost-user protocol. SYNOPSIS vhost-device-sound <--socket> <--backend> [-h|--help] [-V|--version] DESCRIPTION A virtio-sound device using the vhost-user protocol. OPTIONS --socket=SOCKET vhost-user Unix domain socket path --backend=BACKEND audio backend to be used [possible values: null, pipewire, alsa] -h, --help Print help -V, --version Print version VERSION v0.2.0 REPORTING BUGS Report bugs to the project's issue tracker: https://github.com/rust-vmm/vhost-device vhost-device-sound 0.2.0 vhost-device-sound(1) Fixes #687 ("Add man page for vhost-device-sound") Resolves: https://github.com/rust-vmm/vhost-device/issues/687 Signed-off-by: Manos Pitsidianakis --- .cargo/config.toml | 2 + Cargo.lock | 25 +++++++ Cargo.toml | 1 + coverage_config_x86_64.json | 2 +- xtask/Cargo.toml | 27 ++++++++ xtask/build.rs | 14 ++++ xtask/src/main.rs | 134 ++++++++++++++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 xtask/Cargo.toml create mode 100644 xtask/build.rs create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index ba63e46b3..7df701a88 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,5 @@ [target.aarch64-unknown-linux-musl] rustflags = [ "-C", "target-feature=+crt-static", "-C", "link-arg=-lgcc" ] +[alias] +xtask = "run --package xtask --" diff --git a/Cargo.lock b/Cargo.lock index fe67bfa59..a438c178d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,6 +322,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clap_mangen" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -1267,6 +1277,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "rstest" version = "0.25.0" @@ -2328,6 +2344,15 @@ dependencies = [ "tap", ] +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "clap", + "clap_mangen", + "toml 0.8.19", +] + [[package]] name = "yansi-term" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 54a1c46e7..3251c06b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,5 @@ members = [ "vhost-device-spi", "vhost-device-template", "vhost-device-vsock", + "xtask", ] diff --git a/coverage_config_x86_64.json b/coverage_config_x86_64.json index 79c169f86..733dd5953 100644 --- a/coverage_config_x86_64.json +++ b/coverage_config_x86_64.json @@ -1,5 +1,5 @@ { "coverage_score": 86.60, - "exclude_path": "", + "exclude_path": "xtask", "crate_features": "" } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 000000000..e0e5c4eac --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "xtask" +version = "0.1.0" +authors = ["Manos Pitsidianakis "] +description = "A helper binary crate following the cargo-xtask workflow recommended in " +repository = "https://github.com/rust-vmm/vhost-device" +readme = "README.md" +license = "EUPL-1.2 OR GPL-3.0-or-later" +edition = "2021" +publish = false + +[dependencies] +clap = { version = "4.5", features = ["derive"], optional = true } +clap_mangen = { version = "0.2.24", optional = true } +toml = { version = "0.8.19", optional = true } + +[build-dependencies] + +[features] +default = ["vhost-device-sound"] +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"] + +[lints.rust] +unexpected_cfgs = { level = "allow", check-cfg = ['cfg(feature, values("alsa-backend", "pw-backend"))'] } diff --git a/xtask/build.rs b/xtask/build.rs new file mode 100644 index 000000000..658320731 --- /dev/null +++ b/xtask/build.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later +// Copyright (c) 2024 Linaro Ltd. + +fn main() { + #[cfg(feature = "vhost-device-sound-pipewire")] + println!("cargo::rustc-cfg=feature=\"pw-backend\""); + #[cfg(feature = "vhost-device-sound-alsa")] + println!("cargo::rustc-cfg=feature=\"alsa-backend\""); + #[cfg(any( + feature = "vhost-device-sound-pipewire", + feature = "vhost-device-sound-alsa" + ))] + println!("cargo::rustc-cfg=target_env=\"gnu\""); +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 000000000..4b9c8deaf --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later +// Copyright (c) 2024 Linaro Ltd. + +// the `explicit_builtin_cfgs_in_flags` lint must be allowed because we might +// emit `target_env = "gnu"` in build.rs in order to get some cfg checks to +// compile; it does no harm because we're not using anything target_env specific +// here. If we find out that it affects the xtask's crate dependencies (e.g. +// when using this xtask under musl), we should figure out some other solution. +#![allow(unknown_lints, explicit_builtin_cfgs_in_flags)] + +use std::error::Error; + +#[cfg(feature = "mangen")] +use clap::CommandFactory; +#[cfg(feature = "mangen")] +use clap_mangen::Man; +#[cfg(feature = "mangen")] +use toml::value::Table; + +// Use vhost-device-sound's args module as our own using the #[path] attribute + +#[cfg(any( + feature = "vhost-device-sound-pipewire", + feature = "vhost-device-sound-alsa" +))] +#[path = "../../vhost-device-sound/src/args.rs"] +mod vhost_device_sound; + +fn main() { + if let Err(err) = run_app() { + eprintln!("{}", err); + std::process::exit(-1); + } +} + +fn run_app() -> Result<(), Box> { + let task = std::env::args().nth(1); + match task.as_deref() { + #[cfg(feature = "mangen")] + Some("mangen") => mangen()?, + _ => print_help(), + } + Ok(()) +} + +fn print_help() { + eprintln!( + "Tasks: + +{mangen}", + mangen = if cfg!(feature = "mangen") { + "mangen builds man pages using clap_mangen under target/dist/man" + } else { + "" + }, + ) +} + +#[cfg(feature = "mangen")] +fn mangen() -> Result<(), Box> { + let workspace_dir = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(1) + .unwrap() + .to_path_buf(); + let dist_dir = workspace_dir.join("target/dist/man"); + let _ = std::fs::remove_dir_all(&dist_dir); + std::fs::create_dir_all(&dist_dir)?; + + let mut generated_artifacts = vec![]; + + #[cfg(any( + feature = "vhost-device-sound-pipewire", + feature = "vhost-device-sound-alsa" + ))] + { + 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::()?; + + let name: &'static str = manifest["package"]["name"] + .as_str() + .unwrap() + .to_string() + .leak(); + let version: &'static str = manifest["package"]["version"] + .as_str() + .unwrap() + .to_string() + .leak(); + let repository: &'static str = manifest["package"]["repository"] + .as_str() + .unwrap() + .to_string() + .leak(); + let description: &'static str = manifest["package"]["description"] + .as_str() + .unwrap() + .to_string() + .leak(); + let cmd = ::command() + .name(name) + .display_name(name) + .bin_name(name) + .version(version) + .about(description); + let man = Man::new(cmd); + let mut buffer: Vec = Default::default(); + man.render(&mut buffer)?; + clap_mangen::roff::Roff::new() + .control("SH", ["REPORTING BUGS"]) + .text(vec![format!( + "Report bugs to the project's issue tracker: {repository}" + ) + .into()]) + .to_writer(&mut buffer)?; + + let man_path = dist_dir.join("vhost-device-sound.1"); + std::fs::write(&man_path, buffer)?; + generated_artifacts.push(man_path); + } + if generated_artifacts.is_empty() { + println!("No manpages were generated! Try using the correct xtask cargo features."); + } else { + println!("Generated the following manual pages:"); + for art in generated_artifacts { + println!("{}", art.display()); + } + } + + Ok(()) +} From f0c5d93a416c78b11b40bb246746a13128957b3c Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 7 Dec 2024 10:54:14 +0200 Subject: [PATCH 3/8] scmi: move CLI arg types to lib submodule Currently, the main CLI struct type is defined in src/main.rs thus inaccessible when using the crate as a library. Move it to its own module under src/args.rs This will be useful in the followup commit that will use src/args.rs directly to generate manual pages using clap_mangen from the xtask crate in this workspace. Signed-off-by: Manos Pitsidianakis --- vhost-device-scmi/src/args.rs | 32 ++++++++++++++++++++++++++++++++ vhost-device-scmi/src/main.rs | 35 ++++++----------------------------- 2 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 vhost-device-scmi/src/args.rs diff --git a/vhost-device-scmi/src/args.rs b/vhost-device-scmi/src/args.rs new file mode 100644 index 000000000..8dcf6d00b --- /dev/null +++ b/vhost-device-scmi/src/args.rs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 +// Based on implementation of other devices here, Copyright by Linaro Ltd. +//! An arguments type for the binary interface of this library. + +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub struct ScmiArgs { + // Location of vhost-user Unix domain socket. + // Required, unless one of the --help options is used. + #[clap( + short, + long, + default_value_if( + "help_devices", + clap::builder::ArgPredicate::IsPresent, + "PathBuf::new()" + ), + help = "vhost-user socket to use" + )] + pub socket_path: PathBuf, + // Specification of SCMI devices to create. + #[clap(short, long, help = "Devices to expose")] + #[arg(num_args(1..))] + pub device: Vec, + #[clap(long, exclusive = true, help = "Print help on available devices")] + pub help_devices: bool, +} diff --git a/vhost-device-scmi/src/main.rs b/vhost-device-scmi/src/main.rs index e390f1e1f..b541d764b 100644 --- a/vhost-device-scmi/src/main.rs +++ b/vhost-device-scmi/src/main.rs @@ -32,6 +32,7 @@ //! --device iio,path=/sys/bus/iio/devices/iio:device0,channel=in_accel //! ``` +pub mod args; mod devices; mod scmi; mod vhu_scmi; @@ -54,39 +55,15 @@ use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; type Result = std::result::Result; -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct ScmiArgs { - // Location of vhost-user Unix domain socket. - // Required, unless one of the --help options is used. - #[clap( - short, - long, - default_value_if( - "help_devices", - clap::builder::ArgPredicate::IsPresent, - "PathBuf::new()" - ), - help = "vhost-user socket to use" - )] - socket_path: PathBuf, - // Specification of SCMI devices to create. - #[clap(short, long, help = "Devices to expose")] - #[arg(num_args(1..))] - device: Vec, - #[clap(long, exclusive = true, help = "Print help on available devices")] - help_devices: bool, -} - pub struct VuScmiConfig { socket_path: PathBuf, devices: DeviceDescription, } -impl TryFrom for VuScmiConfig { +impl TryFrom for VuScmiConfig { type Error = String; - fn try_from(cmd_args: ScmiArgs) -> Result { + fn try_from(cmd_args: args::ScmiArgs) -> Result { let socket_path = cmd_args.socket_path; let mut devices: DeviceDescription = vec![]; let device_iterator = cmd_args.device.iter(); @@ -140,13 +117,13 @@ fn start_backend(config: VuScmiConfig) -> Result<()> { fn print_help(message: &String) { println!("{message}\n"); - let mut command = ScmiArgs::command(); + let mut command = args::ScmiArgs::command(); command.print_help().unwrap(); } fn main() { env_logger::init(); - let args = ScmiArgs::parse(); + let args = args::ScmiArgs::parse(); if args.help_devices { println!("{}", devices_help()); return; @@ -179,7 +156,7 @@ mod tests { -d fake,name=bar" ); let params: Vec<&str> = params_string.split_whitespace().collect(); - let args: ScmiArgs = Parser::parse_from(params); + let args: args::ScmiArgs = Parser::parse_from(params); let config = VuScmiConfig::try_from(args).unwrap(); assert_eq!(&config.socket_path, Path::new(&path)); let devices = vec![ From 532f25680a14adfca7190dc35fcbb54336712661 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 7 Dec 2024 11:07:12 +0200 Subject: [PATCH 4/8] xtask: generate manpage for vhost-device-scmi Add vhost-device-scmi support for the mangen task in xtask binary. This generates a manpage in target/dist/man/vhost-device-scmi.1 The rendered ROFF output looks like: vhost-device-scmi(1) General Commands Manual vhost-device-scmi(1) NAME vhost-device-scmi - vhost-user SCMI backend device SYNOPSIS vhost-device-scmi <-s|--socket-path> [-d|--device] [--help-devices] [-h|--help] [-V|--version] DESCRIPTION vhost-user SCMI backend device OPTIONS -s, --socket-path=SOCKET_PATH vhost-user socket to use -d, --device=DEVICE Devices to expose --help-devices Print help on available devices -h, --help Print help -V, --version Print version VERSION v0.3.0 REPORTING BUGS Report bugs to the project's issue tracker: https://github.com/rust-vmm/vhost-device vhost-device-scmi 0.3.0 vhost-device-scmi(1) Fixes #697 ("Add man page for vhost-device-scmi") Resolves: https://github.com/rust-vmm/vhost-device/issues/697 Signed-off-by: Manos Pitsidianakis --- xtask/Cargo.toml | 3 +- xtask/src/main.rs | 124 +++++++++++++++++++++++++++++----------------- 2 files changed, 80 insertions(+), 47 deletions(-) diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index e0e5c4eac..a2f68f53e 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -17,7 +17,8 @@ toml = { version = "0.8.19", optional = true } [build-dependencies] [features] -default = ["vhost-device-sound"] +default = ["vhost-device-sound", "vhost-device-scmi"] +vhost-device-scmi = [] vhost-device-sound = ["vhost-device-sound-alsa", "vhost-device-sound-pipewire"] vhost-device-sound-alsa = ["mangen"] vhost-device-sound-pipewire = ["mangen"] diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 4b9c8deaf..01ad6b7db 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -26,6 +26,12 @@ use toml::value::Table; #[path = "../../vhost-device-sound/src/args.rs"] mod vhost_device_sound; +// Use vhost-device-scmi's args module as our own using the #[path] attribute + +#[cfg(feature = "vhost-device-scmi")] +#[path = "../../vhost-device-scmi/src/args.rs"] +mod vhost_device_scmi; + fn main() { if let Err(err) = run_app() { eprintln!("{}", err); @@ -56,6 +62,49 @@ fn print_help() { ) } +#[cfg(feature = "mangen")] +fn mangen_for_crate(manifest: Table) -> Result, Box> { + let name: &'static str = manifest["package"]["name"] + .as_str() + .unwrap() + .to_string() + .leak(); + let version: &'static str = manifest["package"]["version"] + .as_str() + .unwrap() + .to_string() + .leak(); + let repository: &'static str = manifest["package"]["repository"] + .as_str() + .unwrap() + .to_string() + .leak(); + let description: &'static str = manifest["package"]["description"] + .as_str() + .unwrap() + .to_string() + .leak(); + let cmd = ::command() + .name(name) + .display_name(name) + .author(None) + .bin_name(name) + .version(version) + .about(description); + let man = Man::new(cmd); + let mut buffer: Vec = Default::default(); + man.render(&mut buffer)?; + clap_mangen::roff::Roff::new() + .control("SH", ["REPORTING BUGS"]) + .text(vec![format!( + "Report bugs to the project's issue tracker: {repository}" + ) + .into()]) + .to_writer(&mut buffer)?; + + Ok(buffer) +} + #[cfg(feature = "mangen")] fn mangen() -> Result<(), Box> { let workspace_dir = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")) @@ -67,8 +116,7 @@ fn mangen() -> Result<(), Box> { let _ = std::fs::remove_dir_all(&dist_dir); std::fs::create_dir_all(&dist_dir)?; - let mut generated_artifacts = vec![]; - + let mut buffers = vec![]; #[cfg(any( feature = "vhost-device-sound-pipewire", feature = "vhost-device-sound-alsa" @@ -80,54 +128,38 @@ fn mangen() -> Result<(), Box> { std::fs::read_to_string(workspace_dir.join("vhost-device-sound/Cargo.toml"))?; let manifest = manifest.as_str().parse::
()?; - let name: &'static str = manifest["package"]["name"] - .as_str() - .unwrap() - .to_string() - .leak(); - let version: &'static str = manifest["package"]["version"] - .as_str() - .unwrap() - .to_string() - .leak(); - let repository: &'static str = manifest["package"]["repository"] - .as_str() - .unwrap() - .to_string() - .leak(); - let description: &'static str = manifest["package"]["description"] - .as_str() - .unwrap() - .to_string() - .leak(); - let cmd = ::command() - .name(name) - .display_name(name) - .bin_name(name) - .version(version) - .about(description); - let man = Man::new(cmd); - let mut buffer: Vec = Default::default(); - man.render(&mut buffer)?; - clap_mangen::roff::Roff::new() - .control("SH", ["REPORTING BUGS"]) - .text(vec![format!( - "Report bugs to the project's issue tracker: {repository}" - ) - .into()]) - .to_writer(&mut buffer)?; - + let buffer = mangen_for_crate::(manifest)?; let man_path = dist_dir.join("vhost-device-sound.1"); + buffers.push((man_path, buffer)); + } + #[cfg(feature = "vhost-device-scmi")] + { + 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::
()?; + + let buffer = mangen_for_crate::(manifest)?; + let man_path = dist_dir.join("vhost-device-scmi.1"); + buffers.push((man_path, buffer)); + } + + if buffers.is_empty() { + println!("No manpages were generated! Try using the correct xtask cargo features."); + return Ok(()); + } + + let mut generated_artifacts = Vec::with_capacity(buffers.len()); + + for (man_path, buffer) in buffers { std::fs::write(&man_path, buffer)?; generated_artifacts.push(man_path); } - if generated_artifacts.is_empty() { - println!("No manpages were generated! Try using the correct xtask cargo features."); - } else { - println!("Generated the following manual pages:"); - for art in generated_artifacts { - println!("{}", art.display()); - } + + assert!(!generated_artifacts.is_empty()); + println!("Generated the following manual pages:"); + for art in generated_artifacts { + println!("{}", art.display()); } Ok(()) From 673f78c4cc7b4580311628064f3e56867ff17b86 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 7 Dec 2024 11:38:26 +0200 Subject: [PATCH 5/8] xtask: add README.md Add documentation in a README.md file. Signed-off-by: Manos Pitsidianakis --- xtask/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 xtask/README.md diff --git a/xtask/README.md b/xtask/README.md new file mode 100644 index 000000000..c54d67f15 --- /dev/null +++ b/xtask/README.md @@ -0,0 +1,22 @@ +# `xtask` - Run tasks with `cargo` + +This binary crate provides support for running useful tasks with `cargo xtask <..>`. + +## `mangen` + +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. + +```session +$ cargo xtask mangen + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s + Running `target/debug/xtask mangen` +Generated the following manual pages: +/path/to/rust-vmm/vhost-device/target/dist/man/vhost-device-sound.1 +/path/to/rust-vmm/vhost-device/target/dist/man/vhost-device-scmi.1 +``` + +The following crates have manual pages built by default: + +- [`vhost-device-sound`](../vhost-device-sound), enabled by the default feature `vhost-device-sound`. + - It can further be fine-tuned with the features `vhost-device-sound-pipewire` and `vhost-device-sound-alsa`. +- [`vhost-device-scmi`](../vhost-device-scmi), enabled by the default feature `vhost-device-scmi`. From e8d85ce9ba88cf37e60cc40b49e8c80eb78a3b0e Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 7 Dec 2024 11:38:54 +0200 Subject: [PATCH 6/8] README.md: add Packaging and distribution section Add Packaging and distribution section that mentions the ability to generate manual pages using the xtask crate in this workspace. Signed-off-by: Manos Pitsidianakis --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 34a896562..ad75b2cd9 100644 --- a/README.md +++ b/README.md @@ -115,3 +115,11 @@ Supporting Xen requires special handling while mapping the guest memory. The It was decided by the `rust-vmm` maintainers to keep the interface simple and build the crate for either standard Unix memory mapping or Xen, and not both. + +## Packaging and distribution + +The [`xtask`](./xtask/) workspace crate provides support for generating ROFF manual pages. + +If the binary you're interested in packaging does not have a manual page +generated you are encouraged to file a bug or even contribute the necessary +changes by filing a pull request. From d1019549be4386790f91a43e600b6416783c0f3f Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 4 Mar 2025 10:05:18 +0200 Subject: [PATCH 7/8] scmi/README.md: convert code blocks to markdown Code blocks were in some kind of rSt format (?). Convert them to markdown. Signed-off-by: Manos Pitsidianakis --- vhost-device-scmi/README.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/vhost-device-scmi/README.md b/vhost-device-scmi/README.md index bcd640373..00ff431db 100644 --- a/vhost-device-scmi/README.md +++ b/vhost-device-scmi/README.md @@ -40,22 +40,21 @@ messages on the standard error output. The daemon should be started first: -:: - - host# vhost-device-scmi --socket-path=scmi.sock --device fake,name=foo +```shell +host# vhost-device-scmi --socket-path=scmi.sock --device fake,name=foo +``` The QEMU invocation needs to create a chardev socket the device can use to communicate as well as share the guests memory over a memfd: -:: - - host# qemu-system \ - -chardev socket,path=scmi.sock,id=scmi \ - -device vhost-user-scmi-pci,chardev=vscmi,id=scmi \ - -machine YOUR-MACHINE-OPTIONS,memory-backend=mem \ - -m 4096 \ - -object memory-backend-file,id=mem,size=4G,mem-path=/dev/shm,share=on \ - ... +```shell +host# qemu-system \ + -chardev socket,path=scmi.sock,id=scmi \ + -device vhost-user-scmi-pci,chardev=vscmi,id=scmi \ + -machine YOUR-MACHINE-OPTIONS,memory-backend=mem \ + -m 4096 \ + -object memory-backend-file,id=mem,size=4G,mem-path=/dev/shm,share=on \ +``` ## Supported SCMI protocols From 68821c91da5314bdbdf86078f46d382fe0ea1578 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 4 Mar 2025 10:08:24 +0200 Subject: [PATCH 8/8] xtask: also generate EXAMPLES section Include "Examples" section from READMEs if they exist into the generated manual page. Signed-off-by: Manos Pitsidianakis --- Cargo.lock | 16 +++++++++++ xtask/Cargo.toml | 3 +- xtask/README.md | 2 ++ xtask/src/main.rs | 70 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a438c178d..0661a8cd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index a2f68f53e..ddd24b34b 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -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"))'] } diff --git a/xtask/README.md b/xtask/README.md index c54d67f15..e4a30f044 100644 --- a/xtask/README.md +++ b/xtask/README.md @@ -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 diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 01ad6b7db..a31f2288a 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -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(manifest: Table) -> Result, Box> { +fn mangen_for_crate( + crate_dir: std::path::PathBuf, +) -> Result, Box> { + 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::
()?; + let name: &'static str = manifest["package"]["name"] .as_str() .unwrap() @@ -94,6 +108,14 @@ fn mangen_for_crate(manifest: Table) -> Result, Box = 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(manifest: Table) -> Result, Box Result> { + 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::>().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> { let workspace_dir = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")) @@ -124,11 +181,7 @@ fn mangen() -> Result<(), Box> { { 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::
()?; - - let buffer = mangen_for_crate::(manifest)?; + let buffer = mangen_for_crate::(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> { { 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::
()?; - - let buffer = mangen_for_crate::(manifest)?; + let buffer = mangen_for_crate::(workspace_dir.join("vhost-device-scmi"))?; let man_path = dist_dir.join("vhost-device-scmi.1"); buffers.push((man_path, buffer)); }