Mutually exclusive feature flags

Hi!

I have a library crate that I am using in a binary crate. The library crate is used to do music analysis, and there are two providers possible to decode songs: ffmpeg, and symphonia.

In my library crate, they should not necessarily be mutually exclusive (even though that is what makes the most sense), but in my binary crate they should, since the user should actually be choosing between one of the two providers to decode tracks.

The Cargo.toml of my library crate looks (kind of) like this:

[features]
default = ["ffmpeg", "aubio-static"]
# Enable song decoding with ffmpeg. Activated by default, and needed for
# almost all use-cases, disable it at your own risk!
# It is only useful if you want to implement the decoding of the tracks yourself
# and just feed them to bliss, so you don't depend on ffmpeg.
ffmpeg = ["dep:ffmpeg-next", "dep:ffmpeg-sys-next"]
aubio-static = ["bliss-audio-aubio-rs/static"]
# Build ffmpeg instead of using the host's.
build-ffmpeg = ["ffmpeg-next/build"]
ffmpeg-static = ["ffmpeg-next/static"]
# Use to access the Symphonia-based alternative decoder
symphonia = ["dep:symphonia", "dep:rubato"]

[dependencies]
ffmpeg-next = { version = "7.1.0", optional = true }
ffmpeg-sys-next = { version = "7.0.2", optional = true, default-features = false }
symphonia = { version = "0.5.4", optional = true, default-features = false, features = [
    "opt-simd",
] }
...

but I have issues making a Cargo.toml that makes sense for my binary crate, where symphonia and ffmpeg should be mutually exclusive. This is what I have so far (bliss-audio is my library crate):

[features]
default = ["bliss-audio/library", "bliss-audio/aubio-static", "ffmpeg"]
# Build for raspberry pis
rpi = ["bliss-audio/rpi"]
ffmpeg = ["bliss-audio/ffmpeg"]
# If you want to use the pure rust symphonia decoder, instead of ffmpeg.
# Saves you an external dependency and the hassle of packaging, but is
# slightly slower and less accurate.
symphonia = ["bliss-audio/symphonia-all", "bliss-audio/library", "bliss-audio/aubio-static"]

[dependencies]
bliss-audio = {version = "0.10.1", default-features = false}

I feel a bit cumbersome, since people have to use --no-default-features --features=symphonia to enable it.
But maybe there's no better way?

I'd also be interested in some feedback on e.g. if the features on the library crates could be more ergonomic :slight_smile:

Cheers!

1 Like

It looks like Rust has this:

#[cfg(all(feature = "foo", feature = "bar"))]
compile_error!("feature \"foo\" and feature \"bar\" cannot be enabled at the same time");

But your question is different. You want to know if you can do this with Cargo. What if you tried this, and made neither of them default?

#[cfg(all(feature = "foo", feature = "bar"))]
compile_error!("feature \"foo\" and feature \"bar\" cannot be enabled at the same time");

#[cfg(not(all(feature = "foo", feature = "bar")))]
compile_error!("you must enable either the feature \"foo\" or \"bar\"");

Or, maybe you have ffmpeg in default features, but you make symphonia have priority, although this doesn’t toss that external dependency.

However, that link from earlier also says never to do this unless absolutely required. Perhaps try one of the recommendations:

  • Split the functionality into separate packages.
  • When there is a conflict, choose one feature over another. The cfg-if package can help with writing more complex cfg expressions.
  • Architect the code to allow the features to be enabled concurrently, and use runtime options to control which is used. For example, use a config file, command-line argument, or environment variable to choose which behavior to enable.