I'm developing a CLI tool with several subcommands for different use cases. Here's my project structure:
project/
├── Cargo.toml
└── crates/
├── project-foo/
│ └── src/
│ └── lib.rs (subcommand for 'foo')
├── project-bar/
│ └── src/
│ └── lib.rs (subcommand for 'bar')
└── project-core/
└── src/
└── main.rs (main entry point for the CLI app)
Within my Cargo.toml
, I've set up a workspace configuration like this:
[workspace]
members = ["crates/*"]
resolver = "2"
For example, consider the project-foo
crate, which exposes a single subcommand named FileAction
. This subcommand contains two sub-subcommands.
#[derive(Debug, Subcommand)]
pub enum FileAction {
Copy(CopyCommand),
Paste(PasteCommand),
}
#[derive(Debug, clap::Args)]
pub struct CopyCommand {
// Include any relevant options here.
}
impl CopyCommand {
pub async fn handle(&self) -> Result<()> {
// handle the code
}
}
In the project-core
crate, we manage these sub-subcommands using the Cli
struct.
#[derive(Parser)]
#[command(subcommand_required = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
File {
#[clap(subcommand)]
action: FileAction,
},
}
fn main() {
let matches = Cli::command().get_matches();
let cli = Cli::from_arg_matches(&matches).unwrap();
match cli.command {
File { action } => match action {
FileAction::Copy(copy_args) => {
if let Err(err) = copy_args.handle().await {
eprintln!("{}", err);
}
},
FileAction::Paste(paste_args) => {
if let Err(err) = paste_args.handle().await {
eprintln!("{}", err);
}
},
},
}
}
Ideally, my project-core/Cargo.toml
would look something like this:
[package]
name = "project"
version = "0.1.0"
edition = "2021"
[dependencies]
project-foo = { path = "../project-foo" }
project-bar = { path = "../project-bar" }
This approach doesn't work due to a persistent dependency issue between the crates used in the two subcommands. It doesn't seem likely that this issue will be resolved in the near future.
Note: Cargo features can't be used to solve the problem because of Feature unification
My initial idea was to move one crate outside of the crates
folder so that it isn't automatically compiled when I run cargo build
. Then, I'd like to find a way to build it from project-core
and execute it as a subprocess in the CLI defined in project-core/main.rs
.
Is this approach even feasible, and is it a good solution? Or is there a more straightforward way to resolve this issue
I managed to resolve this by patching the crate causing the issue.
In a scenario where two dependencies, such as
project-foo
andproject-bar
, rely on different versions of a third crate, a dependency conflict arises.Each crate specifies a specific version, leading to this conflict. Fortunately, I found a commit in the third crate that fulfilled both crates' version requirements. I added a patch to my
Cargo.toml
to address this:However, if no suitable commit existed, I would have resorted to forking the crate and making modifications to resolve the issue.