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 207083d8f..92474e9c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -301,6 +301,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +[[package]] +name = "clap_mangen" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -1180,6 +1190,12 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "rstest" version = "0.23.0" @@ -2124,6 +2140,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 8b84948cb..1dff327eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ members = [ "vhost-device-spi", "vhost-device-template", "vhost-device-vsock", + "xtask", ] 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. 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![ diff --git a/vhost-device-sound/src/args.rs b/vhost-device-sound/src/args.rs new file mode 100644 index 000000000..acdd4d37b --- /dev/null +++ b/vhost-device-sound/src/args.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause +//! An arguments type for the binary interface of this library. + +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: String, + /// 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 755f6c82d..69b2cf25f 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; @@ -49,7 +50,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}; @@ -190,16 +191,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); @@ -262,6 +253,14 @@ pub struct SoundConfig { audio_backend: BackendType, } +impl From for SoundConfig { + fn from(cmd_args: args::SoundArgs) -> Self { + let socket = cmd_args.socket.trim().to_string(); + + Self::new(socket, false, cmd_args.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 a6f0047ec..1fa415bdc 100644 --- a/vhost-device-sound/src/main.rs +++ b/vhost-device-sound/src/main.rs @@ -1,37 +1,14 @@ // Manos Pitsidianakis // Stefano Garzarella // SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause -use std::convert::TryFrom; 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: String, - /// 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 { - let socket = cmd_args.socket.trim().to_string(); - - Ok(SoundConfig::new(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()); @@ -40,19 +17,12 @@ fn main() { #[cfg(test)] mod tests { + use clap::Parser; use rstest::*; + use vhost_device_sound::BackendType; use super::*; - impl SoundArgs { - fn from_args(socket: &str) -> Self { - SoundArgs { - socket: socket.to_string(), - backend: BackendType::default(), - } - } - } - fn init_logger() { std::env::set_var("RUST_LOG", "trace"); let _ = env_logger::builder().is_test(true).try_init(); @@ -61,12 +31,12 @@ mod tests { #[test] fn test_sound_config_setup() { init_logger(); - let args = SoundArgs::from_args("/tmp/vhost-sound.socket"); + let args = SoundArgs { + socket: "/tmp/vhost-sound.socket".to_string(), + 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(), "/tmp/vhost-sound.socket"); } @@ -89,10 +59,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); } } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 000000000..a2f68f53e --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,28 @@ +[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-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"] +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/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`. 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..01ad6b7db --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,166 @@ +// 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; + +// 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); + 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_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")) + .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 buffers = 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 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); + } + + assert!(!generated_artifacts.is_empty()); + println!("Generated the following manual pages:"); + for art in generated_artifacts { + println!("{}", art.display()); + } + + Ok(()) +}