How to workaround dependency conflicts in a Rust Cargo workspace for a CLI tool?

167 Views Asked by At

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

1

There are 1 best solutions below

0
On

I managed to resolve this by patching the crate causing the issue.

In a scenario where two dependencies, such as project-foo and project-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:

[patch.crates-io.third_crate]
git = "https://github.com/user/third_crate"
rev = "<commit>" # This commit meets both crates' version requirements

However, if no suitable commit existed, I would have resorted to forking the crate and making modifications to resolve the issue.